- Common Lispの
nilは偽・空リスト・シンボルという三つの顔を持ち、JavaScriptのfalse/null/undefinedが別々に存在するのとは対照的です。 - Cの
falseやNULLが数値のゼロに根ざしているのに対し、nilは特定のオブジェクトとの同一性で偽を判定するため、0や空文字列は条件式で真になります。 nilはCのNULL終端に似た役割を持ちますが、番兵ではなくリストの一形態そのものであり、listpは真を返しながらconspは偽を返します。
1. nilはリストのNULL終端なのか?
Common Lispのリストは、C言語の連結リストに似ています。
値の型をいったん無視すれば、C言語での以下のようなNodeを繋いだ実装に対応できます。
typedef struct Node {
int value;
struct Node *next;
} Node;Code language: C++ (cpp)
(cons 1 (cons 2 (cons 3 nil)))
=> (1 2 3)
[ 1 | * ]-->[ 2 | * ]-->[ 3 | * ]--> nil
CAR CDR CAR CDR CAR CDR
[ value | next ]-->[ value | next ]-->[ value | next ]--> NULL
1 2 3Code language: PHP (php)
このリストの要素を再帰的に表示するなら、Common Lispでは、
(defun print-list (xs)
(when xs
(print (car xs))
(print-list (cdr xs))))Code language: Lisp (lisp)
Cなら、このように書けます。
void print_list(Node *p) {
if (p) {
printf("%d\n", p->value);
print_list(p->next);
}
}Code language: C++ (cpp)
xs が nil のとき、p が NULL のときに再帰呼出しは終了します。
そこで、なんとなく nilは「0みたいなものだろう」あるいは「連結リストの末尾NULLに近いはずだ」とイメージします。
ただ、その想定では、腑に落ちないnilの振る舞いもあり、整理しておく必要があります。
1.1. nilが持つ3つの顔
Common Lisp の nil は、データ表現と条件分岐の両方に深く関わる、特別な値です。
多くのプログラミング言語では、
真偽値の false、
参照がないことを表す null、
未定義を表す undefined が分かれています。
Common Lisp では、そうした役割の多くを nil が受け持っています。
- 偽(条件式で偽として扱われる唯一の値)
- 空リスト(
()と同じもの) - シンボル
NIL(それ自体が名前を持つシンボル)
CLHSは、nilについて「COMMON-LISPパッケージに属するシンボルであり、booleanとしての偽であり、空リストであり、空型の名前でもある」と定義しています1。
| 項目 | Common Lisp | C言語 | Java | Python | JavaScript | Ruby |
|---|---|---|---|---|---|---|
| 偽の基本値 | nil | 0 | false | False | false | false, nil |
| 空リストの真偽値 | 偽 (nil) | なし(配列は別概念) | (同上) | 偽 | 真 | 真 |
0 の真偽値 | 真 | 偽 | (条件式に使えない) | 偽 | 偽 | 真 |
| 空文字列の真偽値 | 真 | なし(ポインタ次第) | (同上) | 偽 | 偽 | 真 |
| オブジェクト参照なし | nil が近い | NULL | null | None | null | nil |
undefined 系の値 | 専用値なし | なし | なし | なし | undefined | なし |
1.2. nilなら真を返す関数(null, not, endp)
nilに真と返す関数は、null、not、endpの3種類あり、文脈によって使い分けます。
どれも、「nil なら真、それ以外なら偽」を返します。
null は nil 判定に使う基本の関数です。
「空リストであるか」、あるいは「有効なオブジェクトが束縛されているのか」という意味で使われます。
(null nil) ;=> T
(null '(1 2 3)) ;=> NIL
(null "abc") ;=> NILCode language: Lisp (lisp)
notは、で真偽を反転する関数で、主に論理演算の文脈で使われます。
(not nil) ;=> T
(not t) ;=> NIL
(not 12) ;=> NIL
(not "abc") ;=> NILCode language: Lisp (lisp)
nullやnotは、LIST型やBOOLEAN型である必要はなく、数値や文字列などのオブジェクトでも受け入れます。
リスト操作では、終端の確認に endp で nil かを判定します。
(endp nil) ;=> T
(endp '(1 2)) ;=> NILCode language: Lisp (lisp)
ただし、endpには、型チェックがあります。
proper listの終端チェックに使うもので、引数はリスト型である必要があります。
(endp t) ;=> TYPE-ERROR
(endp 12) ;=> TYPE-ERRORCode language: Lisp (lisp)
2. nilは 0 ではない
Cでは、「偽」や「無効」を表す値は、数値 0 の別名として機能しています。
0 // 整数のゼロ
false // <stdbool.h>では 0
NULL // ヌルポインタ定数。多くの場合 0 または (void*)0
'\0' // 文字列終端の文字コードは 0Code language: Arduino (arduino)
いずれも、機械語レベルで「ゼロかどうか」という比較に帰着し、偽の判定が数値の性質に根ざしています。
これは、繰り返し処理の終端判定に適した設計になっているからです。
初期の機械語レベルでは、n回の繰り返しを「n から 1ずつ減算して 0 でなければ繰り返しの先頭に処理を戻す」という形でコードにすることが多かったからです。
ちなみに、CのNULLはソースコード上では0や((void*)0)として定義されますが、実行時のヌルポインタのビット表現が全ビット0になることをC標準は保証していません2。
一方、Common Lispでは、nilは、C言語のfalse や NULL と違って 0 ではありません。
(zerop nil) ;=> TYPE-ERROR ; 0 かどうかCode language: Lisp (lisp)
0は真で、条件式で偽になるのはnilだけで、
(if 0 "yes" "no") ;=> "yes"
(if "" "yes" "no") ;=> "yes"
(if #() "yes" "no") ;=> "yes"
(if nil "yes" "no") ;=> "no"Code language: Lisp (lisp)
同様に、空文字列も、空ベクタも、すべて真です。
Cが「数値ベース」の偽判定を採用しているのに対して、Common Lispは「オブジェクトベース」だと言えます。
nilという特定のオブジェクトとの同一性で偽を判定しています。
2.1. 連結リストのNULL終端との違い
nilは、NULLポインタとも違います。
Common Lispのリストは、C言語の連結リストに近い構造です。
Cの連結リストでは、末尾のノードのnextがNULLを指します。
Lispのリストでは、末尾のコンスセルのcdrがnilで、
この点では「nilは連結リストのNULL終端に近い」といえそうです。
Lispのcons cell ≒ Cのstruct node
Lispのcdr ≒ Cのnextポインタ
Lispのnil ≒ CのNULL終端
再帰処理の終了条件としてnilが自然に機能するのも、この設計によります。
(defun my-length (lst)
(if lst
(+ 1 (my-length (cdr lst)))
0))Code language: Lisp (lisp)
if lstは「lstがnilでなければ続ける」という意味です。
空リストの判定と偽の判定が一致しているため、終了条件を別途用意する必要がありません。
ただし、nilとNULL終端には、決定的な違いがあります。
CのNULLは「次のノードがない」ことを示す「番兵」です。
リストそのものの値ではなく、リンクの終端を表すポインタ値として使われます。
一方、Lispのnilは、終端マーカーであると同時に、それ単体で空リスト()という値です。
リストとは「nilか、cdrがリストであるコンスセル」で、nilは(無効な)番兵ではなく、リストの一形態そのものです3。
Cで言えば、NULLを「空リストを表すヘッドポインタ」として使うことはできます。
struct Node *xs = NULL; // 空リストCode language: Arduino (arduino)
この使い方はLispのnilに近いのです。
ただ、常に NULL がそのように使われるとは限らず、末尾の終端としてだけ使われることもあり、値としての地位は一定していません。
2.2. nilはundefinedではない
ちなみに、JavaScriptなどの言語では、undefinedのように「未定義」を表す専用の値があります。
しかし、Common Lispのnilは、それとも別です。
Common Lisp では、「まだ値が入っていない」、つまり変数が未束縛(unbound)であれば、nil ではなく、参照するとエラーになります。
2.3. 機械語レベルのnil表現
C言語の false や NULL と Common Lisp の nilは、概念としては「数値ベース」と「オブジェクトベース」という違いがあります。
では機械語レベルでは、どのように違うのでしょうか。
Cの関数をコンパイルしたアセンブリを見てみます。
int check(int x) {
if (x) return 10;
else return 25;
}Code language: Arduino (arduino)
check:
pushq %rbp
movq %rsp, %rbp
movl %edi, -4(%rbp)
cmpl $0, -4(%rbp) ; 0と比較
je .L2
movl $10, %eax
jmp .L3
.L2:
movl $25, %eax
.L3:
popq %rbp
retCode language: Intel x86 Assembly (x86asm)
cmpl $0, -4(%rbp)で、引数が0かどうかを比較しています。
SBCLで同等の処理を逆アセンブルしてみます。
(disassemble
(lambda (x)
(if x 10 25)))Code language: Lisp (lisp)
; disassembly for (LAMBDA (X))
CMP RSI, R12 ; NIL
MOV EDX, 20
MOV EAX, 50
CMOVE EDX, EAX
LEAVE
CLC
RETCode language: Intel x86 Assembly (x86asm)
CMP RSI, R12のR12は、SBCLがnilの内部表現を保持するために使うレジスタです。
SBCLのx86-64実装では、R12レジスタはスレッドローカルな特殊変数バインディング領域へのポインタに使われており、nilの表現もそこから参照されます4。
つまり、Cが$0という即値のゼロと比較するのに対して、
SBCLはR12に保持されたnilのアドレス領域と比較しています。
Common Lispでは、真偽の判定に「ゼロかどうか」ではなく、ちゃんと「nilかどうか」判定しています。
ただ、「オブジェクトベースだから遅い」ということにはなりません。
SBCLのようにnilをレジスタに保持すれば追加コストはほぼなく、両者ともCPUの整数比較命令に収束しているからです。
3. nilは「空リスト」である
nilは、「コンスセルではないのに、リストである」という性質もあります。
(null nil) ;=> T ; nilかどうか
(listp nil) ;=> T ; リストかどうか
(consp nil) ;=> NIL ; コンスセルかどうか
(atom nil) ;=> T ; atomかどうかCode language: Lisp (lisp)
3.1. nilはコンスセルではない特別なリスト
興味深いのは、listp nilがTを返すのに、consp nilはNILを返すことです。nilはリストですが、コンスセルではないからです。
リストは、再帰的に定義されています。
list ::= nil
| cons cell whose cdr is a listCode language: PHP (php)
つまり、list型とは、cons型またはnull型、ということです。
そのこめ、CLHSではlistpを(typep object '(or cons null))と等価と定義しています5。
ちなみに、nilを指すコンスセル(nil . nil)はどうなのでしょう。
(cons nil nil)
;=> (nil)
(consp (cons nil nil)) ;=> T
(null (cons nil nil)) ;=> NILCode language: Lisp (lisp)
(nil . nil)は、nilそのものではなく、要素を1つ持つリスト(nil)のことです。nilは「要素が0個のリスト」であり、両者はまったく別のものです。
3.2. nilやtはシンボルである
nil とシンボルの関係は、ちょっと複雑です。
nilは、コンスセルに属さないリストで、null型はcons型には属しません。
Common Lispでは、コンスセルでないものはすべてatomです6。
nilやtはatomですが、シンボル(SYMBOL型)の中の真偽値(BOOLEAN型)に属します。
tは、BOOLEAN型で、nilは、さらに特化したNULL型です。
(type-of t) ;=> BOOLEAN
(symbolp t) ;=> => T BOOLEAN型は、SYMBOLのサブタイプ
(type-of nil) ;=> NULL ; NULL型のオブジェクト
(subtypep nil 'BOOLEAN) ; => T NULL型は、BOOLEANのサブタイプ
(symbolp nil) ; => T NULL型は、SYMBOLのサブタイプCode language: Lisp (lisp)
ちなみに、クォートした 't や'nil は、ほかのシンボルと違って t と nil そのものです。
(type-of 'abc);=> SYMBOL
(type-of 't) ;=> BOOLEAN
(type-of 'nil);=> NULL
(eq nil nil) ;=> T ; 同一のオブジェクト
(eq t 't) ;=> T
(eq nil 'nil) ;=> T ; シンボルと同じ
(eq nil :nil) ;=> NIL ; キーワードシンボルではないCode language: Lisp (lisp)
tは、BOOLEAN型で、NULL型と異なり、リストではありません。
(listp t) ;=> NILCode language: Lisp (lisp)
ちなみに、(symbolp t)は真なのに、(subtypep t ‘SYMBOL)で判定すると、NILと判定されます。
(subtypep t 'SYMBOL) ;=> NILCode language: Lisp (lisp)
これは、tを直接渡すと型指定子として解釈されるからです。
t型(任意型)は、すべての型の上位型を意味するためNILが返ります。
| 値 | type-of | サブタイプの関係 |
|---|---|---|
nil | NULL | NULL ⊂ BOOLEAN ⊂ SYMBOL ⊂ ATOM ⊂ TNULL ⊂ LIST ⊂ T |
t | BOOLEAN | BOOLEAN ⊂ SYMBOL ⊂ ATOM ⊂ T |
4. nilで失敗を返す設計慣習
Common Lispの関数には「成功すれば役に立つ値を返し、失敗すればnilを返す」という設計の慣習があります。
(find 3 '(1 2 3 4 5)) ;=> 3
(find 9 '(1 2 3 4 5)) ;=> NIL
(member 3 '(1 2 3 4 5)) ;=> (3 4 5)
(member 9 '(1 2 3 4 5)) ;=> NILCode language: Lisp (lisp)
memberは、「要素が見つかれば、その要素から始まるリストの末尾を返す。見つからなければnilを返す」ので、条件式でそのまま使えます7。
つまり、nilは、偽・空リストだけでなく、「失敗」の意味も兼ね備えています。
(if (member 3 '(1 2 3 4 5))
"found"
"not found")
;=> "found"Code language: Lisp (lisp)
4.1. 区別が必要になる場面
nilにいろいろな役割が集まることで、意味の読み分けが必要になる場面があります。
関数がnilを返したとき、それが「条件が偽だった」「空のリストだった」「検索に失敗した」のどれなのかは、文脈で判断するしかありません。
「見つからなかった」のか「見つかった値がnilだった」のかを区別したい場合は、nilだけに頼れません。
(defun lookup (key alist)
(let ((pair (assoc key alist)))
(if pair
(values (cdr pair) t)
(values nil nil))))Code language: Lisp (lisp)
複数値で「値」と「見つかったかどうか」を分けて返すのが、こういう場面での定石です。
CLHSではvaluesを使って複数の値を同時に返すことができ、受け取り側はmultiple-value-bindで分解できます8。
5. まとめ
nilは0ではありません。
Cが数値のゼロ性で偽を判定するのに対して、Common Lispはnilというオブジェクトとの同一性で判定します。
機械語レベルでは両者ともに整数比較に収束しますが、意味論レベルでの設計は根本的に違います。
nilはCのNULL終端に近い役割を果たしますが、番兵ではありません。
リストの再帰的な定義においてnilは空リストそのものであり、終端マーカーとしてだけでなく値として存在しています。
nilがコンスセルではなくatomであり、同時にリストであり、シンボルでもあるという多面性は、Lispがリスト処理を中心に発展してきた言語設計の帰結です。
偽・空・失敗を一つの値に寄せることで、条件分岐とデータ処理が自然につながっています。
- nilは空型(empty type)の名前でもあります。空型はすべての型のサブタイプであり、要素を持ちません。nilという型とnilというオブジェクトは別の概念です。 – CLHS 1.4.1.4.4 NIL
- C標準(6.3.2.3節)は「値が0の整数定数式、またはvoid*にキャストされたそのような式をヌルポインタ定数と呼ぶ」と定義しています。実行時のヌルポインタの表現は実装依存です。 – Don’t use NULL – Jens Gustedt’s Blog
- CLHSでは
conspについて(consp object) == (typep object 'cons) == (not (typep object 'atom))と定義しています。nilはatom型に属するためconsではなく、この定義からもリストの終端がコンスセルでないことが導かれます。 – CLHS: Function CONSP - x86では
%fsセグメントレジスタがスレッドローカルストレージに使われ、x86-64ではR12が同様の役割を担います。 – SBCL Internals: Implementation (Linux x86) (listp object) == (typep object 'list) == (typep object '(or cons null))。listpはconsかnullであれば真を返し、ドットリスト(proper listでないもの)に対しても真を返します。 – CLHS: Function LISTP- CLHSでは
atomを(not (consp object))と等価と定義しています。(atom object) == (typep object 'atom) == (not (consp object))。atomはconsでないすべてのオブジェクトを指します。 – CLHS: Function ATOM memberはリストの先頭から順に要素を調べ、条件を満たす要素が見つかればその位置以降のリスト全体を返します。 – CLHS: Function MEMBERvaluesは複数の値を返す標準的な方法です。(values 1 2)は1と2という2つの値を返します。受け取る側はmultiple-value-bindを使います。 – CLHS: Accessor VALUES