無駄と文化

実用的ブログ

JavaScript で関数とクラスを見分ける

JavaScript においては typeof や constructor を参照するだけでは関数と class を見分けることができません。

// ただの関数
function myFunction() {}

// class
class MyClass {}

// typeof では見分けられない
typeof myFunction;  // => 'function'
typeof MyClass;     // => 'function'

// constructor でも見分けられない
myFunction.constructor;  // => Function
MyClass.constructor;     // => Function

関数と class を見分けるには toString() すると良いです。

myFunction.toString();  // => 'function myFunction() {}'
MyClass.toString();     // => 'class MyClass {}'

class の場合、toString() した結果が 'class ' から始まります。

もし関数と class を判別する関数を書くとしたらこのようになります。

function isFunction(obj) {
  return typeof obj === 'function' && !obj.toString().match(/^class /);
}

function isClass(obj) {
  return typeof obj === 'function' && obj.toString().match(/^class /);
}

isFunction の方の条件を obj.toString().match(/^function /) にしていないのは、アロー関数を toString() した場合に結果が 'function ' から始まらないからです。

 

なぜこんなことになっている?

なぜ typeof や constructor だけでは関数と class が見分けられないのでしょうか。
それは class は本質的に関数と同じものだからです。

一昔前 [いつ?] まで JavaScript には class 構文が無くて、人々は function 構文を使って class 相当のものを書いていました。

例えばこのように、

class Rectangle {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }

  area() {
    return this.height * this.width;
  }
}

// ↓↓↓ かつてはこう書いていた

function Rectangle(height, width) {
  this.height = height;
  this.width = width;
}

Rectangle.prototype.area = function() {
  return this.height * this.width;
};

メソッドを定義するときに .prototype を参照しています。このあたりの構文が JavaScript がプロトタイプベース言語であることを思い出させてくれますね。 *1

このように私たちが class 構文で定義しているものは関数でもあるので、二つを見分けるのには特殊なテクニックが必要になってしまっています。

 

区別する必要はない?

この件を ChatGPT に聞いてみると「JavaScript において class は関数と同等なので二つを区別する必要はありません」と言われました。本当でしょうか?

もう一度 function 構文を用いて定義した Rectangle クラスのコードを引用します。

// コンストラクタ
function Rectangle(height, width) {
  this.height = height;
  this.width = width;
}

// area メソッドの定義
Rectangle.prototype.area = function() {
  return this.height * this.width;
};

コンストラクタの中では return で値を返すことをしていません。そのため Rectangle(2, 3) のように new を付けずに関数として Rectangle を呼び出すと返り値は undefined になってしまいます。

というわけでやはり関数と class の区別は必要ですね。おのれ ChatGPT 。

 

まとめ

function 構文を用いた class 定義、久しぶりに書いたのでとても懐かしい気持ちになりました。

 

 

私からは以上です。

*1:実際には現代もこのあたりの事情はまったく変わっていなくて、 class 構文は単なる function 構文のシンタックスシュガーです

自然数の集合が無限集合であることを証明する

6歳になる子供がいる。ある日、自転車に乗りながらこんな話をした、

👦「ねぇ、数字って数えていったらどこまで続くの?」
👨『いい質問だね、どう思う?』
👦「うーん、どこまでも続く?」
👨『そう、数字はどこまでも続くから 1, 2, 3, ... と数えていっても終わりはないんだよ』

本当だろうか?確かめてみよう。

 

目次

 

自然数の集合 ℕ は無限集合であると定義されているか?

自然数の定義はいろいろあり得るが、代表的かつ簡単な定義はこうだ。

次の公理を満たす集合 ℕ の元を自然数と云う

集合 ℕ と定数 o と関数 S について、
 1. o ∈ ℕ
 2. 任意の n ∈ ℕ について S(n) ∈ ℕ
 3. 任意の n ∈ ℕ について S(n) ≠ o
 4. 任意の n, m ∈ ℕ について n ≠ m ならば S(n) ≠ S(m)

この公理を ペアノの公理 と云う。

普段、私たちが素朴に "自然数" と呼んでいるものはこの公理を満たす。
定数 o とは 0 のことで。 関数 S とは「n に対して n の次の数を返す関数」のことだ。

集合 ℕ : 自然数の集合、0, 1, 2, ...
定数 o : 0
関数 S : S(n) = n + 1

こうなる。 *1 *2

さて自然数の定義を見てみたが「自然数は無限にある」とは書かれていなかった。
定義にも公理にも無い以上、自然数が無限にあると主張するには証明が必要だ。

というわけで証明してみよう。

 

ペアノの公理の気持ちを理解する

証明にはペアノの公理を使う。証明に取りかかるまえにペアノの公理の気持ちを理解しておこう。
公理 1~4 の "気持ち" を文章にするとこうだ、

  1. o ∈ ℕ
    → 集合 ℕ には少なくともひとつの要素 o が在る
  2. 任意の n ∈ ℕ について S(n) ∈ ℕ
    → 全て自然数には「次の数」が在って、「次の数」もまた自然数だ
  3. 任意の n ∈ ℕ について S(n) ≠ o
    → ただし o は「次の数」にはならない、o の「前の数」は無い
  4. 任意の n, m ∈ ℕ について n ≠ m ならば S(n) ≠ S(m)
    → 「次の数」が他の人と被ることはない

さらにペアノの公理の集合 ℕ を絵に描いてみよう、

graph LR;
    O((o)) -->|"S(o)"| N1(("n₁"));
    N1(("n₁")) -->|"S(n1)"| N2(("n₂"));
    N2(("n₂")) -->|"S(n₂)"| N3(("n₃"));
    N3(("n₃")) -->|"S(n₃)"| NEXT[どこまでも続く...];

o から始まって次の数 (関数 S による変換) を辿っていくとどこまでも続く。この「次の数チェーン」を辿った全体が自然数の集合 ℕ になる。

 

自然数が無限にあることは自明か

先ほどの絵を見ていると「うーん、自然数が無限にあるのは自明だなぁ」と思う読者もいるんじゃないか。
気持ちは分かる。さきほどの絵がきれいすぎるのが良くない。

今度はもう少し汚い絵を描いて「自然数ってもしかしたら有限かも」という気持ちも味わってみよう。
次に見ていくのはどれも "このようにはならない" という例である。

次の数チェーンが途切れる

もしも次の数チェーンが途中で途切れていたらどうだろう。その場合は「自然数は有限個」ということになりそうだ。

graph LR;
    O((o)) -->|"S(o)"| N1(("n₁"));
    N1(("n₁")) -->|"S(n₁)"| N2(("n₂"));
    N2(("n₂")) -->|"S(n₂)"| N3(("n₃"));
    N3(("n₃")) --->|...| Nz(("nₓ"));

nₓ には次の数が無い。なのでチェーンはここで終わり。

ということにはならない。公理 2 が「全ての自然数に次の数があり、かつ、次の数もまた自然数である」ことを要請してくれている。

次の数チェーンが袋小路にはまる

次の数チェーンの先がループになっている場合も問題がありそうだ。

graph LR;
    O((o)) -->|"S(o)"| N1(("n₁"));
    N1(("n₁")) -->|"S(n₁)"| N2(("n₂"));
    N2(("n₂")) -->|"S(n₂)"| N3(("n₃"));
    N3(("n₃")) --->|...| Nz(("nₓ"));
    Nz(("nₓ")) -->|"S(nₓ)"| N2(("n₂"));

全ての自然数に次の数があるという公理を満たしつつ有限個になってしまった。

このようなことも起こらないはずだ。公理 4 によって次の数が被らないことが要請されている。 S(n₁)S(nₓ) の矢印がどちらも n₂ に向いているのは公理 4 に反している。

次の数チェーンが元の場所に戻ってくる

次の数チェーンがぐるっと一周して元の場所 o に戻ってくる場合はどうだろう。

graph LR;
    O((o)) -->|"S(o)"| N1(("n₁"));
    N1(("n₁")) -->|"S(n₁)"| N2(("n₂"));
    N2(("n₂")) -->|"S(n₂)"| N3(("n₃"));
    N3(("n₃")) --->|...| Nz(("nₓ"));
    Nz(("nₓ")) -->|"S(nₓ)"| O((o));

チェーンは途切れていないし、次の数が被ってもいない。でもループができてしまった。

やはりこのようなことも起こらない。公理 3 が「o は次の数にならない」ことを要請している。 S(nₓ) の矢印が o に向いているのはおかしなことだ。

 

ここまでどうにか自然数有限個の危機は回避してきた。が、自然数が無限に存在していることは全然自明ではない気がしてきた。ちゃんと証明して心から安心したいところだ。

 

自然数の集合 ℕ が無限集合であることを証明する

というわけでようやく証明にとりかかろう。

背理法で示す。ペアノの公理を満たす有限集合 ℕ が存在し、その要素数が k であると仮定する。

関数 S によって ℕ の各要素が別の要素に移る様を書き並べると。

 \mathbb N \ni o \rightarrow n_{\, w} \in \mathbb N
 \mathbb N \ni n₁ \rightarrow n_{\, x} \in \mathbb N
 \mathbb N \ni n₂ \rightarrow n_{\, y} \in \mathbb N
 \vdots
 \mathbb N \ni n_{\, k-1} \rightarrow n_{\, z} \in \mathbb N

左辺に着目すると、公理 2 により全ての要素を S によって移せるため k 個の要素全てが現れていることになる。
一方右辺に着目すると、公理 3 により右辺には o が現れないため高々 k-1 個の要素しか現れない。

S によって k 個の要素が高々 k-1 個の要素に移されるため、 鳩の巣原理 によりある i, j が存在し、nᵢ ≠ nⱼ, S(nᵢ) = S(nⱼ) となる。しかしこれは公理 4 に矛盾する。

よって仮定に誤りがあり、ペアノの公理を満たす集合 ℕ が存在すればそれは無限集合であることが示された。

 

まとめ

子供の質問にちゃんと答えるのは難しい。

 

 

私からは以上です。

*1:自然数を 1 から始める流儀もあるが、この記事では 0 から始まる前提で進める。1 から始まる流儀を採用しても以降の議論は同じになる

*2:余談だがペアノの公理を満たすような (ℕ, o, S) の組は他にもある、何なら無限に存在するらしい

10月の体重推移

ビーチサンダルが寒いので最近は靴を履くようになりました。
10月の体重の推移を振り返ります。

9月中の体重推移のグラフ。月初には78.8kgだった体重が月末には76.8kgまで落ちている6ヵ月間のグラフ。計測を始めた8月末から減少トレンドを維持して約8kg減

9月20日から10月31日にかけて 78.8kg→76.8kg (マイナス2.0kg) という推移でした。

6ヵ月間のグラフを見るとどうにか下降トレンドであることは分かる。

 

運動の記録

10月のエクササイズ時間をまとめたグラフ。10月中に47回, 累計28時間56分32秒間のエクセサイズをして、11,425kcal消費している

10月の消費は11,425kcalでした。目標通り10,000kcalを超えられてよかった。

 

10月のウォーキングの記録。30回, 累計23時間14分41秒のウォーキングで113km歩いて8,268kcal消費している 10月のランニングの記録。2回, 累計54分12秒のウォーキングで8km歩いて681kcal消費している

ウォーキングとランニングを抽出した記録です。10月は合計で121kmでした。
こちらも目標にしていた100kmを超えられてよかった。

 

前回の振り返り

これまでの振り返りはこんな感じでした。

blog.mudatobunka.org

blog.mudatobunka.org

 

まとめ

エクササイズ時間・消費cal・移動距離ともに減ってます。
有酸素運動を減らして筋トレを増やしたのが要因です。が、単純に飽きつつある気がしますね。
気を引き締めてがんばります。

今月も消費10,000kcal, ウォーキング+ランニングで100kmを目指せたらいいかなと思います。

 

年内にあと6kg落として体重70kgを切りたい。がんばるぞ💪

 

 

私からは以上です。