まあ文献を参照したわけでもない自己流解釈なので間違ってても責任は持てません。
###きっかけのツイート
F#のunitってC#のvoidと同じく戻り値がないことを意味するのかと思ってた。
— のさ_AWSとFlutter学習中 (@nosa_programmer) March 16, 2019
けどunitという値が返るから厳密には違うらしい。
scalaもunit使うし関数型言語はそういうものなのかな?(*´ω`*)
###ユーザー定義関数などなかった時代
初期のFORTRANやBASICではCPUのCall命令などで呼ばれるサブルーチンの扱いは大まかにこんな風に分かれていた。
- ステートメント
- ファイルやコンソールの入出力など。組み込み命令で値を返さない。結果を受け取るものは格納先のバッファや変数を引数として渡す
- ユーザー定義サブルーチン
- 任意のコード。引数や返り値はないのでデータのやりとりはグローバル変数を経由して行う
- 関数
- sin,cosのように数値計算したり、文字列の切り出しなどを行い、その結果を返り値として返す。初期のころは組み込み関数しか存在しなかったと思う
そしてPascalあたりでユーザー定義関数も作れるようになった。
###全部関数でいいだろ
これらのサブルーチンコールは引数や返り値の有無による違いはあるけどCPU命令レベルで見ればわりと似たようなもので、ある程度統一したルールを作れば共通化できる。
そこでCではサブルーチンをすべて「関数」として扱うことにした。
ちなみにK&R時代のCは引数や返り値の型チェックは一切なく、returnで値を返さない関数の返り値は「不定」だったらしい。
その後ANSI Cができるあたりでvoid
が導入され、返り値の型をvoidとして宣言した関数の返り値を受け取ろうとするとコンパイル時にエラーとなった。
###値を返さない関数は困る
話は変わって関数型言語。関数型言語がどういうものかという話はもっとちゃんとした説明を読んでほしいけど、関数型言語というものが新たに作られた理由、何を実現しようとして関数型言語が生まれたのかというと「ある処理の結果を別の処理に渡すというのは"データの流れ"であり、それらはひとつの式として表せる」という発想があった。
//古典的スタイル
var resultA = funcA();
var resultB = funcB(resultA);
var resultC = funcC();
var resultD = funcD(resultB, resultC);
var resultE = funcE();
return resultD + resultE;
//ひとつの式にする
return funcD(funcB(funcA()), funcC()) + funcE();
なのでそれ以前の手続き型言語との対比で言えば「式型言語」と名付けてれば誤解は少なかったかもしれない。
このように一連のサブルーチンコールの流れを一つの式としてまとめようとするとき、「値を返さない関数」が混じっているとまとめられない。値を返さない関数を呼ぶ前と呼んだ後の2つの式に分断されてしまう。
Cの型チェックを厳密にしたときの、「値を返さない関数の返り値を受け取ろうとするとエラー」という仕様が足かせになった。
それでも値を返す必要がない、返すべき値がない処理というのはいくらでも存在する。そういう関数をコールすると式が分断されるのは関数型言語が目指したものを破壊してしまう。
そこで「値を返さない関数」を廃止するためにこれを「式を分断させないために形の上だけで値を返す、でも返した値は使わない(使ってもエラーにはしない)」とすることにした。
そのために生まれた「"値なし"を表す値」がLISP系ならNIL
,javascriptならundefined
,その他多くの関数型言語ではunit
と名付けられたもの。
今になって振り返ると「Cにvoidを導入したときにそうしておけばよかったのに」という話になるけど、Cは組み込み機器などでも使うために「バイト単位で命令数をケチりたい」という事情もあって「使いもしない値を返すための命令」でコードが増えるのを嫌ったし、その当時はそんなことをする意味があるとも思えなかったのだろう。