前提
TaPL 読みながら Haskell/Denotational semantics 見て、自分なりに納得した己が解釈を__文芸的に__綴っていくだけの内容です
序: void, null, undefined, (), [], ⊥
どんな言語でも_「空っぽ」なり「無い」なり「未定義」_なりを意味する値が用意されており、型付けが動的だろうと静的だろうとこれらに対してだけはヤケに自由度の高い用法を許されています。
Haskell の undefined と ( ) 型
Haskellであっても、以下のコードは「正しく型づけされ、正しく動作する」としてコンパイルが通ります
fib :: Integer -> Integer
fib 0 = 1
fib 1 = 1
fib n
| n > 1 = fib (n - 2) + fib (n - 1)
| otherwise = undefined -- (n < 0)
なお、この時の undefined
はこういう型で存在しています
undefined :: a -- どんな型であっても良い
ついで、Haskell には__「何も返さない事を意味する型」__として()
(Unit型と読む)モノがあります。main
の型がIO ()
です。putStrLn :: String -> IO ()
であったり、IOが関わる処理では割と頻出します
※この内容はあくまでも『考察』です※
javascirptのグローバル変数undefined
,null
、RubyのNilClass
、Objective-Cのnil
、Javaのnull
…様々な言語に其々の「空」や「無」や「未定義」を意味する値が用意されていますが、そもそも何でこうした値を用意しなくてはいけなかったのか? という点について振り返ってみます
null とは
往々にして、各自のプログラミング遍歴で null
に持つ_「意味」_は異なってくると思います
-
null
は__「空」/「無」__を意味する -
null
は__「未定義値」__を意味する -
null
は__インスタンスが無かった時のアレ__ x.do(some) unless x
でブロック進んじゃってグエーってなるヤツ==
!
の結果でfalsyとして扱われてコーディングややこしくさせてるヤツ
この記事の主題は**「なんで、こんなに厄介な存在が要るん?」**について、それなりに「しょうがにゃいにゃあ」と思える解釈を綴るものです
「空」/「無」/「未定義」の無い世界
はじめに、C言語で考えてみます。尤も、この手の話だったらALGOLとかCOBOLやPascalの話もしないといけないのでしょうけれど、それらへは大きく語れる知見を持ってはいないので…
true/false ? no, it's "0 or not."
extern int
doSomething()
{
/* ... */
}
int
main()
{
while(0) {
/* ここは絶対に実行されない */
doSomething();
}
return 0;
}
C言語そのものにはbool型というのが提供されておらず、条件分岐は 0
か 0
以外を真偽値として扱います。(C99からは #include <stdbool.h>
すれば一応bool型を使えますが、それでもif(){}
やfor(;;){}
、while(){}
にはそのままint
のリテラルが使えます)
enum
による列挙にしても、実体は整数ですし、基本的に 整数の値をどのように解釈するか というハンドリングが求められます。TaPL では「C言語の静的型検査の型システムは安全ではない」と述べられていますが、次に続く内容でその一端を触れる事が出来ます
void* NULL
C言語仕様では、malloc
でメモリ確保に失敗した時に返すヌルポインタは処理系に応じてその判断が要る為、stdlib.h
から NULL
を利用し、同値判定させるコトで「失敗」を判定させるのが慣習となっています
(NULL
はC89から整数定数0
か(void*) 0
で表現されるものと定義されていますが、コンパイル後にどうなるかは実装依存とされています)
#include <stdlib.h>
/* ... */
int* ptr = (int*)malloc(sizeof(int) * len);
if (NULL == ptr) {
exit(EXIT_FAILURE);
}
/* ... */
free(ptr);
ptr = NULL;
/* ... */
int is_empty(void* ptr) への実装自由度の問題
なお、NULL
に実値のあるオブジェクトのポインタや関数のポインタを割り当ててはいけないというのがあり、それは安心できるのですが、__「それが空っぽであるかどうか」__は、プログラマの設計に裁量が求められます
- 構造体は__グローバル領域で宣言されているが、中身は空っぽ__
- 構造体は__スタック領域の中で宣言されており、中身は空っぽ__
- 構造体としての変数のポインタが
NULL
である__(ヒープ領域にアドレス確保してない)__ - 構造体としての変数のポインタが
NULL
である__(アドレス確保に失敗した!)__ - 変数のポインタはヒープアドレスには存在するが
- a.メンバーのポインタは未だ
NULL
(メンバーへのmalloc
忘れてる) - b.メンバーのアドレスも確保されているが、値は未だ代入されていない
- a.メンバーのポインタは未だ
- アドレスはまだヒープ領域から参照できるが、単に
free()
の後にNULL
代入するの忘れた(ヤバイ)
この中で確実に_「不手際」なのは 6. くらいですが、それ以外については_「ライブラリ利用者がどう介入したか?」_によって「ヘマ」か「設計ミス」か「バグ」_か分かれます。転じて、__コードの読み手からは実態を推し量りにくい__とも言えます。
書き手と読み手に強いるモノ
というわけで、言語設計上で「空」「無」を意味するモノが無い__という事は、プログラマに「何を以ってして空とするか」を自己責任で決めさせる__事を意味します
そして、__「空」だけでなく「意図的に、その時点では”無い”状態である」と、「この時点では未定義としか言い様のないモノである(しかし宣言はしないといけない)」__ことを、__ライブラリを読み、利用する人__へも理解を要求する事になります
ライブラリのソースにコメントなどがあれば、それを確認する事で設計者の意図を組みとれる術はありますが、既にダイナミックリンクライブラリとしてバイナリ化されてしまったモノとなると困難なはずです
そこで__利用する側がコンパイル時や実行時に知る事が出来る、利用上の注意点である、メタな情報__としての__「型」と「値」__のアイデアが活きてきます
次回 「”無”が有る世界」
モチベーションと反響があったら続き書きます。
Lisp の '()
, Objective-C の nil
, RDB(SQL)のNULL
, Python の None
等々の話から、「無い」を意味する値の意味 について
見識違いへのツッコミ等
ありがとうございます、ご教授願います