JavaScript の every メソッド
少し前に、 JavaScript のメソッドについて話題になっていました。
この every
メソッドは、他の言語だと any
とか forall
とかの名前で実装されている関数・メソッドとほぼ同じもので、「その配列の中の全ての要素が条件を満たすか」を判定し真偽値で応えてくれるものです。
every
メソッドを、空配列をレシーバにして呼び出した時に何が起こるか、という観点が注目されていました。これは、常に真を返します。たとえ恒偽式が与えられていようとも、です。
[].every(_ => false); //=> true
このような挙動について、動きが分かりやすいかどうか、理に適っているかどうか、納得できるかどうか、という点が議論の焦点になっていたように思います。
私個人としては、この挙動は分かりやすく、理に適っていて、納得できるものに思えました。
数学的観点からの every
メソッド
数学的に見ると、このメソッドは「全ての」( for all
)つまり ∀
記号と同じ意味です。
そして、空集合に対して「全ての要素が任意の条件を満たしているか」という判定は必ず真になります。
……この説明、上記の MDN の解説ページにもちゃんと書いてありました(私は空虚に真という用語は知りませんでした)。
つまり every
メソッドが空配列に対して常に真を返すのは、数学的には理に適っているし納得できるものだということです。
畳み込みとしての every
メソッド
every
メソッドは「集合をブール論理、単位元を true
、二項演算を論理積とするモノイドに対する畳み込みである」と解説している方もいました。
詳しい説明は避けますが、これは要するに「最初に単位元(この場合は true
)を用意し、コレクションから要素を次々と取り出して与えられた関数に適用した後、その結果と前の結果との二項演算(この場合は論理積)を行う」ということです。
……と言葉で書いても何だかよく分らないので、コードで書きましょう。畳み込みは、 fold
とか reduce
という名前の関数・メソッドで多くの言語に実装されており、 JavaScript にもあります。
[1, 10, 100].reduce((res, _) => res && false, true); //=> false
[].reduce((res, _) => res && false, true); //=> true
畳み込みを空のコレクションに対して適用すると、初期値がそのまま返って来ます。 every
メソッドを畳み込みとして見た場合、初期値は true
なので、空配列に対して呼び出せば true
が返ってくるのは当然だ、という話ですね。
他の言語の似たような機能の挙動
参考として他の言語の挙動を見て見ましょう。
と言っても網羅的なものでは全く無く、私が普段触っている幾つかの言語でどういう挙動をするか見てみた程度です。
[1,2,3].all? { false } #=> false
[].all? { false } #=> true
Seq(1,2,3).forall(_ => false) //=> false
Seq().forall(_ => false) //=> true
List.for_all (fun _ -> false) [1; 2; 3];; (* false *)
List.for_all (fun _ -> false) [];; (* true *)
all (\_ -> False) [1, 2, 3] -- False
all (\_ -> False) [] -- True
……正直ちょっと言語が偏りすぎていてあんまり参考にできるものかどうかは自信が無いのですが、いずれの言語でも JavaScript と同様の挙動をしていることがわかります。
私は上に挙げた言語を普段使っているので、それと同じ挙動をする JavaScript の every
メソッドは自然に受け入れることができた、という話ですね。
every
メソッドまとめ
というわけで、数学的観点からも、畳み込みという観点からも、他の言語との比較に於いても、 JavaScript の every
メソッドの挙動はとても自然なものである、という説明ができたかと思います。
……ここまでが本記事の 前座 となります。
JavaScript の min
max
関数
上の話題から派生して、「 JS は min
max
関数の挙動も面白い」みたいな話が流れてきていました。
動きを見てみましょうか。
Math.min(1,2,3); // もちろん 1 が返る
Math.min(); // Infinity が返る……!?
Math.max(1,2,3) // もちろん 3 が返る
Math.max(); // -Infinity が返る……!?
この挙動に関して、私は違和感を覚えました。言い換えると、「あまり嬉しい挙動ではない」と感じました。
しかしどうしてそう感じてしまったのでしょうか?
というのを、ここから説明していきたいと思います。
畳み込みとしての min
max
関数
この JavaScript の min
max
関数の挙動は、上の every
で行ったように畳み込みとして説明ができるという解説がありました。
同じことを2度書く必要は無いので、以下では min
の場合にのみ限って話を進めていきましょう。
min
関数の挙動を畳み込みとして書くと、以下のようになるでしょう。
[1,2,3].reduce((res, elm) => res < elm ? res : elm, Infinity);
この時、 every
メソッドの解説で使った「二項演算」と「単位元」を考えてみます。
「二項演算」は、「2つの値のうち、小さい方を返す」という関数になります。
「単位元」は、他のどの数と一緒に上の二項演算に適用しても相手の数を返す値なので、 JavaScript の number の中では Infinity
がこれに当たります。
さて、空のコレクションに対して畳み込みを行った場合、単位元を返すのでしたね。
よって、 Math.min()
関数を引数無しで呼び出した時には Infinity
が返るのは当然、という話です。
……確かに、畳み込みとして見ると min
関数の挙動はとても自然なものに思えます。
「どのようにするか」より「何をするか」
ところで、 min
関数は「畳み込み」なのでしょうか?
今まで散々そういう説明をしてきたじゃないかという話なのですが、これは「そういう実装になっている」という話であって、関数の使い手が、それを畳み込みであると認識している、あるいはする必要があるという主張とは区別できるでしょう。
プログラミング言語で処理を関数にまとめることの利点に、その処理を抽象化できるという点があります。
何か大きなプログラムを作る際に、本質的ではない処理の実装に思考力を取られることを避けて、実際に行いたい処理にだけ集中できるという利点ですね。
よく「関数の説明には『どうやったか』ではなくて『何をするのか』を書け」とか、「 How ではなく What 」とか言われているやつです。
一度、内部実装の話を頭から追い出して、素朴な使い手として min
関数を見て見ましょう。
我々はこの関数を「2つの要素から小さい方を返す畳み込み」として使いたいのでしょうか?
もっと単純に、 このコレクションの中から一番小さい要素を返す処理 として使いたいのではないのでしょうか?
数学的観点からの min
max
関数
※この項で使ってる用語とかの定義はこの辺りを参考にしていますが、何分数学には疎いので、何か間違いがあったらごめんなさい
大抵の関数の使い手は、おそらく内部でどのような処理を行っているかには興味が無く、この関数を「コレクションの中の一番小さな要素を返してくれる」ものとして利用しているでしょう。
(勿論、パフォーマンスを求めてカリカリにチューニングしたい場合とかは別でしょう。競プロ用途とか。で、それは「大抵の関数の使い手」から外れていますよね?)
具体的な動きではなくて、もっとふわっとした意味で使っていることが多いと思います。
ただ、定義がふわっとしたままだと扱いにくいので、 every
の時でも行ったように数学的な定義を援用して話を進めてみましょう。
数学的に見ると、 min
関数は (半順序?)集合の最小元を求める 操作と言えるでしょう。
「一番小さな要素を求める」という素朴な感覚と、そんなに離れていない処理だと思います。
では、「 Math.min()
関数を空引数で呼び出す」に相当する操作は何でしょうか?
これは、 空集合の最小元を求める ことになるでしょう。
なるほど。一体何になるのでしょうか。
……いろいろ調べたのですが、よく分りませんでした。
いかがでしたか?
何故分からないかというと、そもそも「集合の最小元」というのが 空ではない集合に対して 定義してあるものだからですね。
「最小元を求める処理」には「空集合でない場合に限る」という注釈が付くわけです。
前提条件を満たしていないわけですね。
こういう場合、「最小元は存在しない」あるいは「操作が未定義」とでも言えばいいのでしょうか。
つまり、数学的に言えばおそらく、「 Math.min()
関数を空引数で呼び出す」という処理は許されていないわけです。
他の言語との比較
許されていない処理を行った場合に何が起こるのか。
これは一種の未定義動作ですね。
未定義動作なので、 JavaScript の関数がどんな値を返しても文句を言われる筋合いはないのかもしれません。
それを踏まえた上で、では他の言語ではどのような挙動をするのだろうか、という点を確認してみましょう。
またしても私が常用している言語を適当に見てみただけなので、何ら説得力を持つ調査結果ではないのですが……。
[1,2,3].min #=> 1
[].min #=> nil
Seq(1,2,3).min //=> 1
Seq.empty[Int].min //=> java.lang.UnsupportedOperationException: empty.min
Seq.empty[Int].minOption //=> None
# 普段使わないのだけど、気になったので調べてみた
min(1,2,3) #=> 1
min() #=> TypeError: min expected 1 argument, got 0
min([1,2,3]) #=> 1
min([]) #=> ValueError: min() arg is an empty sequence
// こっちも普段は使ってないけど調べてみた
Collections.min(Arrays.asList(1, 2, 3)); //=> 1
Collections.min(new ArrayList<Integer>()); //=> java.util.NoSuchElementException
minimum [1,2,3] -- 1
minimum [] -- Exception: Prelude.minimum: empty list
※ OCaml の標準ライブラリにはコレクションの最大値・最小値を求める関数は無さそうでした。
これを見ると、「存在しないことを表す値を返す( Ruby, Scala の minOption
)」か、シンプルに「例外を投げる」( Python, Java, Haskell, Scala の min
)かという挙動になっていますね。
どうして私は違和感を覚えたのか
上の他言語との比較が分かりやすいのですが、私が普段使っている言語だとこういう場合に、空の値を返すにしろ例外を投げるにしろ、正常ではない挙動をするのですね。例えば Ruby は型が厳格な言語なので、数値系の演算に nil
を引数として与えると大抵の場合は例外が飛びます。
そういう言語に慣れていたために、 Infinity
という 数値として意味のある値 が返ってくる仕様に驚きを感じ、知らない物を排除する本能から「好ましくない」と感じたのだと思われます。
個人的には、空配列の最小値を求めるような処理を行うようなことはまず無く(そもそもそんな値は存在しないのですから)、それを行おうとした段階で何かが間違っているので、空の値を返すなりさっさとエラーを吐いてくれるなりした方が使い勝手が良いなぁと感じます。
余談
ここからは完全に余談です。
本文以上にテキトーなことを言っているので気をつけてください。
最小元、極小元、下限、下界
どれも定義が違います。数学むつかしい……。
記事中で「空集合の最小元は存在しない」と書きましたが、実は下限は存在します。
https://en.wikipedia.org/wiki/Infimum_and_supremum
( Wikipedia の記事ですが典拠が付いています。)
空集合に対する下限は ∞
であり、また上限は -∞
ですね。
JavaScript の Math.min()
関数は下限を得る関数だった……?
(下限は infimum
なので関数名が min
なのと整合性が取れないのですが……。)
min
max
は二項演算?
記事中では min
max
関数を畳み込み演算として扱いましたが、 そもそも min
max
は二項演算では? という方もいらっしゃるかもしれません。
min
を畳み込みとした際のモノイドの二項演算を「2つの値のうち、小さい方を返す演算」としましたが、これこそがまさに min
関数だろ、という話ですね。
実際、可変長引数を許さない Haskell や OCaml では、 min
max
関数は厳密に2引数を取る関数であり、型としても閉じているので、二項演算です(……よね?)。
42 `min` 99 -- 42
42 `max` 99 -- 99
確かに、二項演算とそれを使った畳み込みは別物で、別名が付いている場合も多いかもしれません。
例に挙げた Haskell では、二項演算としての min
関数と、そのコレクションに対する畳み込みとしての minimum
関数がそれぞれ用意されているわけです。
一方で Lisp なんかだと、二項演算の関数を畳み込みとしても使えることがあるような気がします。
例えば台集合を整数、二項演算子を +
、単位元を 0
とした時、畳み込みは sum
関数などとして定義されていることが多いですが、 Lisp 系の言語だと +
関数がそのまま sum
として働きます。
(+ 1 2) ;=> 3
(+ 1 2 3 4) ;=> 10
なので、「二項演算子的な関数を可変長引数にすると畳み込みになる」みたいな考え方は割とあるのかなぁ、と考えるなどしました。
可変長引数? コレクション?
他言語の例に出した Ruby Scala Haskell の min
( Haskell は minimum
)メソッド・関数は配列やリストのようなコレクションを取るのに対して、 JavaScript の Math.min()
は可変長引数ですよね。
これを混同しても良いものかどうか少し悩んだのですが、何かご指摘があった際に考えることとします……( Ruby も Scala も関数じゃなくてコレクション自体のメソッドだし、仮に関数だとしてもこいつらはコレクションをメソッドの可変長引数として渡せるし、そんでもって受け取る時はコレクションになってるし、 Haskell はそもそも可変長引数が無いし、この辺の差異は今回の話題とはあんまり関係無い所だと思うけどそういう部分に突っ込み飛んできたらうーん……
謝辞
Slack 等で知人のエンジニアの方々に大いに助けていただきました。
ありがとうございます