JavaScript
関数型プログラミング

javascriptのMath.max()で-Infinityが返る理由

javascriptは謎仕様の多さで有名だ。

https://twitter.com/cdesio/status/1013166206877163520

その中でも目を引くのがこの仕様。

> Math.min() 

< Infinity
> Math.max()
< -Infinity

何だこれは、クソ仕様か。


minが∞でmaxが-∞と。


時空が歪む。

なぜ天下のjavascript様がこのような素晴らしい仕様を採用しているか調べたところこのような記事が。

https://charlieharvey.org.uk/page/why_math_max_is_less_than_math_min

この記事を読みjavascriptの深淵な仕様が理解できたのでここに記す。


Math.min() or Math.max()は要素をn→1へと飛ばす関数

知識豊かな皆さんはすでにMath.min()とMath.max()がどういった関数なのかは百も承知だろう。

const list = [1, 2, 3, 4, 5];

const max = Math.max(...arr);
const min = Math.min(...arr);

console.log(max);
// 5
console.log(min);
// 1

複数の要素を入れると最大値or最小値を返す。


単純明快。

だが待ってほしい。


javascriptはfunctionalな世界。


functionとしての彼らの実態を理解しているか?


といってもそんなに難しいものではない。


いってしまえばn個の引数から1つを選んで返す関数。しゃぞーと言っても構わない。


この性質が重要だ。


n→1関数としてのreduce

ではn→1な関数を実装するときjavascriptではどう実装するだろうか。


配列に対して作用する関数はmap、forEach、filter、reduce。


そのなかでn→1なもの。


そう、reduceだ。

const list = [1, 2, 3, 4, 5];

const sum = (accumulator, currentValue) => accumulator + currentValue;

console.log(list.reduce(sum, 0))
// 15

初期値をもとに複数要素を結合していき最終的に1要素を吐き出す。


Math.min()とMath.max()の実装にぴったりだ。


二項演算子 + 単位元 = モノイド

ここで数学のお話。


別に記事を書くページを間違えたわけじゃない。


説明に必要なのだ。


ちなみにモナドとの関係性は不明。


さあここでWikipediaによるモノイドの定義。

集合 S とその上の二項演算 •: S × S → S が与えられ、以下の条件

結合律
S の任意の元 a, b, c に対して、(a • b) • c = a • (b • c).
単位元の存在
S の元 e が存在して、S の任意の元 a に対して e • a = a • e = a.
を満たすならば、組 (S, •, e) をモノイドという。

モノイドの構成に必要なものは任意の集合S、その上での二項演算子、そして単位元。


二項演算子は四則演算子などの2つの要素に作用し1つの要素を吐き出すもの。


単位元はそれと他の任意の要素xを二項演算子にかけたとき常にxを吐き出すようなものだ。


例を挙げよう。

例えば加算演算子+を用いたモノイドの構成。


+は任意の2つの実数を足した数を吐き出す。(二項演算子)


そして足し算は順番を変えても結果は同じ。(結合率)


最後に0は任意の実数$x$に加えても$x$のままである。(単位元)


よって実数集合$\Re$、演算子+、単位元0の組み合わせ$(\Re,+,0)$はモノイド。

簡単なことを難しく言っただけ。


なんの関係がと思うかもしれないが、覚えておいてほしい。


単位元としての-Infinity

ではいよいよMath.max()を構成していこう。


順番としては、


  1. 2つの要素を比べ大きい方を返す関数の作成


  2. 1で作成した関数をreduceで次々に適用


である。


イメージはバブルソートに近い。


具体的には、

const max = (identity, num) => num > identity ? num : identity;

といった関数をreduceで連続して適用すればMath.max()が構成できる。


これは2つの要素に作用し1つの要素を返すためまさに二項演算子だ。


ここで注意すべきはreduceの初期値をどうするかということだ。


reduceの適用1回目は常に入力されたn個の要素の1つめをmaxが返さなければならない。


言い換えるとmax関数が構成するモノイドの単位元をreduceの初期値としなければならない。


ではmaxの単位元はなんだろうか?


そう-Infinityである。


-Infinityはすべての実数より小さい。


すなわち-Infinityと任意の実数$x$をmaxにかけるとmaxは常に$x$を返す。


そしてreduceは入力要素がないと初期値を返す。


これがMath.max()が-Infinityを返す理由である。


以下簡易的なMath.max()。

const max = (identity, num) => num > identity ? num : identity;

const list = [1, 2, 3, 4, 5];
const nullList = [];

console.log(list.reduce(max, -Infinity));
// 5

console.log(nullList.reduce(max, -Infinity));
// -Inifinity

Math.min()は単位元をInfinityとするためほぼロジックは同じ。


まとめ

個人的にreduceを関数として捉え直すという部分が面白かった。


なんにせよ生き残っているものには何かしらの理由があるっぽい。


Thanks for inventing javascript。