【Common Lisp】
nilは0でもNULL終端でもない
(C言語などとの比較)

  • Common Lispのnilは偽・空リスト・シンボルという三つの顔を持ち、JavaScriptのfalse/null/undefinedが別々に存在するのとは対照的です。
  • CのfalseNULLが数値のゼロに根ざしているのに対し、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 LispC言語JavaPythonJavaScriptRuby
偽の基本値nil0falseFalsefalsefalse, nil
空リストの真偽値偽 (nil)なし(配列は別概念)(同上)
0 の真偽値(条件式に使えない)
空文字列の真偽値なし(ポインタ次第)(同上)
オブジェクト参照なしnil が近いNULLnullNonenullnil
undefined 系の値専用値なしなしなしなしundefinedなし

1.2. nilなら真を返す関数(null, not, endp)

nilに真と返す関数は、nullnotendpの3種類あり、文脈によって使い分けます。
どれも、「nil なら真、それ以外なら偽」を返します。

nullnil 判定に使う基本の関数です。
「空リストであるか」、あるいは「有効なオブジェクトが束縛されているのか」という意味で使われます。

(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)

nullnotは、LIST型やBOOLEAN型である必要はなく、数値や文字列などのオブジェクトでも受け入れます。

リスト操作では、終端の確認に endpnil かを判定します。

(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言語のfalseNULL と違って 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の連結リストでは、末尾のノードのnextNULLを指します。
Lispのリストでは、末尾のコンスセルのcdrnilで、
この点では「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でなければ続ける」という意味です。
空リストの判定と偽の判定が一致しているため、終了条件を別途用意する必要がありません。

ただし、nilNULL終端には、決定的な違いがあります。

CのNULLは「次のノードがない」ことを示す「番兵」です。
リストそのものの値ではなく、リンクの終端を表すポインタ値として使われます。
一方、Lispのnilは、終端マーカーであると同時に、それ単体で空リスト()という値です。

リストとは「nilか、cdrがリストであるコンスセル」で、nilは(無効な)番兵ではなく、リストの一形態そのものです3

Cで言えば、NULLを「空リストを表すヘッドポインタ」として使うことはできます。

struct Node *xs = NULL; // 空リストCode language: Arduino (arduino)

この使い方はLispのnilに近いのです。
ただ、常に NULL がそのように使われるとは限らず、末尾の終端としてだけ使われることもあり、値としての地位は一定していません。

2.2. nilundefinedではない

ちなみに、JavaScriptなどの言語では、undefinedのように「未定義」を表す専用の値があります。

しかし、Common Lispのnilは、それとも別です。
Common Lisp では、「まだ値が入っていない」、つまり変数が未束縛(unbound)であれば、nil ではなく、参照するとエラーになります。

2.3. 機械語レベルのnil表現

C言語の falseNULL と 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, R12R12は、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 nilTを返すのに、consp nilNILを返すことです。
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サブタイプの関係
nilNULLNULLBOOLEANSYMBOLATOMT
NULLLISTT
tBOOLEANBOOLEANSYMBOLATOMT

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. まとめ

nil0ではありません。
Cが数値のゼロ性で偽を判定するのに対して、Common Lispはnilというオブジェクトとの同一性で判定します。
機械語レベルでは両者ともに整数比較に収束しますが、意味論レベルでの設計は根本的に違います。

nilはCのNULL終端に近い役割を果たしますが、番兵ではありません。
リストの再帰的な定義においてnilは空リストそのものであり、終端マーカーとしてだけでなく値として存在しています。

nilがコンスセルではなくatomであり、同時にリストであり、シンボルでもあるという多面性は、Lispがリスト処理を中心に発展してきた言語設計の帰結です。
偽・空・失敗を一つの値に寄せることで、条件分岐とデータ処理が自然につながっています。

  1. nilは空型(empty type)の名前でもあります。空型はすべての型のサブタイプであり、要素を持ちません。nilという型とnilというオブジェクトは別の概念です。 – CLHS 1.4.1.4.4 NIL
  2. C標準(6.3.2.3節)は「値が0の整数定数式、またはvoid*にキャストされたそのような式をヌルポインタ定数と呼ぶ」と定義しています。実行時のヌルポインタの表現は実装依存です。 – Don’t use NULL – Jens Gustedt’s Blog
  3. CLHSではconspについて(consp object) == (typep object 'cons) == (not (typep object 'atom))と定義しています。nilはatom型に属するためconsではなく、この定義からもリストの終端がコンスセルでないことが導かれます。 – CLHS: Function CONSP
  4. x86では%fsセグメントレジスタがスレッドローカルストレージに使われ、x86-64ではR12が同様の役割を担います。 – SBCL Internals: Implementation (Linux x86)
  5. (listp object) == (typep object 'list) == (typep object '(or cons null))。listpはconsかnullであれば真を返し、ドットリスト(proper listでないもの)に対しても真を返します。 – CLHS: Function LISTP
  6. CLHSではatom(not (consp object))と等価と定義しています。(atom object) == (typep object 'atom) == (not (consp object))。atomはconsでないすべてのオブジェクトを指します。 – CLHS: Function ATOM
  7. memberはリストの先頭から順に要素を調べ、条件を満たす要素が見つかればその位置以降のリスト全体を返します。 – CLHS: Function MEMBER
  8. valuesは複数の値を返す標準的な方法です。(values 1 2)は1と2という2つの値を返します。受け取る側はmultiple-value-bindを使います。 – CLHS: Accessor VALUES