以前、多値について書きました(まとめはコレ)。今日は無値(値がないこと)について考えてみようかと。小見出しを付けてパランパランと、細かいネタを書いた後で、無値が気になったキッカケとかを話します。
C言語のvoid
Cでは、戻り値がない関数(“手続き”と呼ぶべきかもしれない)を、void foo(int);
のように宣言します(引数名は省略可能)。が、引数がない関数をint bar();
と宣言するのはダメです。引数の仕様が不明なときにbar()
と書く習慣があったので、引数がないことは、int bar(void);
と明示的に書きます。
この書き方だと、引数がなくて戻り値もない関数はvoid baz(void);
と宣言することになりますね。
ML言語のunit型
MLには、単一のメンバーを持つ集合(singleton set)を意味する型 unit があります。unit型に属する唯一の値は(何でもいいのですが)空タプル()となっています。
MLは、print "Hello\n";
のような関数呼び出しも値を持ちます。その値が何であるかは実は問題にはなりませんが、printの戻り値方はuniti型だと考えて、()が戻ると約束します。
JavaScript2.0のVoidとNever
JavaScript2.0では、特殊な値を含む型として、Void, Null, それとNeverが定義されています。
- Void -- 値はundefinedだけ
- Null -- 値はnullだけ
- Never -- 値はまったく無い
Void型とundefined値は、MLにおけるunit型と()値と同じです。undefinedはnullとは別物です。
Neverというのは、ホントに値がないことです。これは、そもそも関数から(正常には)戻ってこれないことを意味します。次の関数をみてください。
function signalError(msg) {
throw new Error(msg);
}
次のコードを考えましょう。
var x = signalError("Help Me!");
// xの値は?
変数xにはいかなる値(undefinedでさえ)も確実に入る保証はないですよね。こんなときに、ホントに値が返らない -- 戻り値はNever型だ、というわけです。
haXeのenum Void型
先日、NekoVMに触れましたが、僕が興味を持ったのはNekoよりは(同じ開発者による)haXe言語(http://haxe.org/)です。haXeはJavaScript/ActionScriptに似た高級言語で、コンパイラはいくつかのターゲット言語コードを生成します。そのターゲット言語のひとつがNekoというわけです。
haXe言語について紹介する機会はまたあるかもしれません(わからんが)。とりあえず気になった点は、Void型の解釈です。haXeはenum(列挙)をサポートしているので、基本的な型のいくつかをenumで定義しています。例えば:
enum Boolean {
true;
false;
}
このBooleanの定義は納得のいくものです。が、Voidを定義するenumはどうも腑に落ちません。
enum Void {
}
Voidは「値がない」ことだから、これでいいのではないか -- いやいや、MLやJavaScript2.0のように、Voidは何か特定の1つの値を持つとみるほうが合理的です。次の定義が適当でしょう。
enum Void {
undefined;
}
「どんな型にもundefined値が暗黙に含まれる」という解釈もありえますが、そうなるとNeverがうまく定義できません(Neverを特殊扱いするしかない)。次のようにするのが自然な気がします。
enum Never {
}enum Void {
undefined;
}enum Boolean {
true;
false;
}
void引数にvoid値を渡せるか
C言語のvoid baz(void)
のような記法は、歴史的事情からの帰結であり、特に明白な意図があって採用されたものではないでしょう。しかし、int bar(void); void baz(void);
という宣言を眺めていると、「barの引数型はvoid、bazの戻り値型はvoid」なのだから、bar(baz())
という入れ子の式が合法的な気がしてきます。まー、錯覚ですけどね:
error: too many arguments to function `bar'
構文的には、引数なし(引数リストの長さが0)と引数の型がvoid(単一の引数の値がundefinedのような特殊値)は違いますから、しかたありません。
JavaScriptのようにアリティ(引数の個数)チェックをしない言語なら、bar(baz())
も許してくれます。ただし、その実行効果は引数評価方式に依存します。引数の評価が終わってから関数本体の評価を行うなら、bar(baz())
という式の実行は順次実行文baz(); bar()
と変わりません。一方、関数本体の評価中に引数値が必要になったら引数評価が行われる方式だと、bar(baz())
がbar()
になることもあります。
詮索してもしょうがない気もするが
「戻り値なし」や「引数なし」の議論は、重箱の隅をつつく話で、あまり実り多いとは思えないのですけど、個人的な好みを書いておけば:
- Void型はsingleton setを表すと考える。
- 戻り値がVoidの関数とは、X → Void の(副作用もあるかもしれない)写像と考える。
- 引数なしの関数は、Void → Xの写像と考える。
- ただし、構文的な「引数なし」と折り合いをつけなくてはならない。
最後の「折り合い」の問題は、関数適用に括弧を使わないなら、f()
は「fに特殊値()を渡した」として解決できるでしょう。そうでないときは、f()
をf(undefined)
の略記と解釈すればいいのかな(確信ないけど)。
無値のハナシも、副作用(状態遷移など)や例外まで考えると多少意味がある内容を持ちそうですが、まー、これくらいにしておきます。