【Common Lisp】
nilとfalse・0・null・undefinedとの
違い
(空リスト)

  • Common Lisp では nil は「偽」であり、同時に「空リスト」でもあります。
  • 条件式では nil だけが偽で、0 や空文字列や空でないリストは真です。
  • 他言語の falsenull、場合によっては「値が見つからない」という役割の一部を、Common Lisp では nil が兼ねています。
  • この設計のメリットは、リスト処理での条件分岐が自然につながることですが、反面、「偽」「ppp空」「値がない」を nil に寄せたことで、意味があいまいになる点に注意が必要です。

関連記事

1. nil とはなにか

Common Lisp の nil は、データ表現と条件分岐の両方に深く関わる、特別な値です。

多くのプログラミング言語では、
真偽値の false
参照がないことを表す null
未定義を表す undefined が分かれています。

それに対して Common Lisp では、そうした役割の多くを nil が受け持っています。

1.1. nil は偽である

Common Lisp の条件式では、nil だけが偽です。

それ以外の値はすべて真として扱われます。
この点は、0 や空文字列が偽になる言語に慣れていると混乱しやすいところです。

(if 0 
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)

1.2. nil は空リストでもある

一方、Common Lisp では、nil は空リストでもあります。

空リストは () とも書けますが、Common Lisp ではこれと nil は同じものとして扱われます。

(equal nil '())
;=> T

(listp nil)
;=> TCode language: Lisp (lisp)

もちろん、nil はリストですが、空でないリストは真になります。

(if '(1 2 3) 
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)

紛らわしいですが、中身が nil のリスト '(nil) は、リスト全体としては空ではないので、条件式では真です。

(if '(nil)
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)

1.3. t は真を表すアトムである

真を表す代表値が t です。

t は空リストではなく、アトム(atom、リストでない値)です。

(atom t)
;=> T

(listp t)
;=> NILCode language: Lisp (lisp)

2. nil と関係の深い関数・概念

nilに真と返す関数は、nullnotendpの3種類あり、文脈によって使い分けます。

2.1. null で nil かどうか判定する

(null nil)
;=> T

(null '(1 2 3))
;=> NILCode language: Lisp (lisp)

nullnil 判定に使う基本の関数です。

2.2. not で真偽を反転する

(not nil)
;=> T

(not 42)
;=> NILCode language: Lisp (lisp)

not は、「nil なら真、それ以外なら偽」を返します。

2.3. endp でリストの終端を調べる

(endp nil)
;=> TCode language: Lisp (lisp)

リスト再帰では、終端が nil であることを前提に endp を使うことがあります。

3. 他の言語の値とどう違うか

Common Lisp の nil を深く理解するために、他の言語の falsenullundefined0 などと比べてみます。

項目Common LispC系言語の一部PythonJavaScriptRuby
偽の基本値nilfalse0Falsefalsefalse, nil
0 は偽かいいえ言語によってははいはいはいいいえ
空文字列は偽かいいえ言語によるはいはいいいえ
空リストは偽かはい (nil)言語によるはい配列は真
null 系の値nil が近い場面ありnull / nullptr などNonenullnil
undefined 系の値専用値なしなしなしundefinedなし

この表は大まかな比較です。
厳密な仕様は言語ごとに違いますが、Common Lisp の特徴は「偽が nil だけであり、それが空リストでもある」という一点に集約されます。

3.1. false との違い

多くの言語では、真偽値として truefalse があります。

false は純粋に「偽」を表します。
たとえば JavaScript の false や Python の False は、リストや配列の空とは別の概念です。

それに対して Common Lisp の nil は、偽であるだけでなく、空リストでもあります。
分離せずに、空リストの表現と偽が nil にまとまっています。

3.2. 0 との違いとアセンブリ

C 系言語では、0 が偽として扱われることがあります。

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)
    je      .L2
    movl    $10, %eax
    jmp     .L3
.L2:
    movl    $25, %eax
.L3:
    popq    %rbp
    ret

このcmpl $0, -4(%rbp)で「0 かどうか」で比較しています。

しかし Common Lisp では、0 は普通の数値であり、真です。

(if 0
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)

このため、「数値が 0 なら失敗」といった感覚をそのまま持ち込むと危険です。

つまり、

  • C言語:0 を偽とする(数値ベース)
  • Lisp:nil のみ偽(オブジェクトベース)

ただし、意味論(semantics)の上では、Common Lisp の if は、「0 かどうか」ではなく「nil かどうか」を判定しますが、機械語レベルでは単なる「値比較」です。
ただし、その値は 0 ではないですが。

SBCL(Steel Bank Common Lisp)の逆アセンブル例では、

(disassemble
 (lambda (x)
   (if x 10 25)))Code language: Lisp (lisp)
; disassembly for (LAMBDA (X))
; Size: 29 bytes. Origin: #xB800CC8DDF                        ; (LAMBDA (X))
; DF:       498B4510         MOV RAX, [R13+16]                ; thread.binding-stack-pointer
; E3:       488945F8         MOV [RBP-8], RAX
; E7:       4C39E6           CMP RSI, R12                     ; NIL
; EA:       BA14000000       MOV EDX, 20
; EF:       B832000000       MOV EAX, 50
; F4:       0F44D0           CMOVE EDX, EAX
; F7:       C9               LEAVE
; F8:       F8               CLC
; F9:       C3               RET
; FA:       CC0F             INT3 15                          ; Invalid argument count trapCode language: CSS (css)

ここで、R12NIL の内部表現で、CMP RSI, R12x == NIL を判定しています。

Lisp は「特定値との比較」になるため、実装によっては定数参照が必要です。
しかし、実は、計算効率の観点では、両者の差はほとんどありません。

SBCL のように nil をレジスタに保持すれば追加コストはほぼなく、どちらも最終的には CPU 的には整数比較という同種の処理になっているからです。

3.3. 空文字列や空ベクタとの違い

同様に、空文字列 "" や空ベクタ #() も、nil ではないので真です。

(if ""
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)
(if #()
    "yes" "no")
;=> "yes"Code language: Lisp (lisp)

Common Lisp では、「空」と「偽」は基本的に同じではありません。
「たまたま空リストだけが nil と一致する」ため、偽でもある、という構造なのです。

3.4. null との違い

他の言語での null は、「値がない」「参照先がない」といった意味で使われることが多いです。

似ていますが、Javaのnullは、無効なメモリアドレスを指し、空リストという意味ではありません。

ただ、Common Lisp では、文脈によって nil が似た役割を果たします。
たとえば検索に失敗したとき、該当要素がないとき、空の結果であるときに nil が返ることがあります。

(find 10 '(1 2 3 4 5))
;=> NILCode language: Lisp (lisp)

ただし、これは「無効だから nil が返された」というより、「見つからなかったことを表す慣習として nil を返している」と見る方が自然です。

3.5. undefined との違い

JavaScript の undefined のように、「未定義」を表す専用の値がある言語もあります。

Common Lisp の nil は、それとは別です。

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

値の種類を細かく分ければ、意味の違いは明確になります。
その代わり、扱う概念が増えます。

Common Lisp は、ある程度の意味を nil に寄せることで、少ない基本概念で一貫して書けるようにしています。
これは厳密な区別よりも、言語全体の統一感を優先した設計と見ることができます。

4. なぜ nil がそこまで多くの役割を持つのか

空リストが「偽」という論理的な意味を持つにいたったのは、Lisp は、リスト処理を中心に発展してきた言語だからです。

リストの末尾は空リストです。
そのため、再帰処理では「空なら終わり」という形がよく出てきます。

この空リストを、そのまま偽としても使えるようにすると、コードが自然になります。

(defun my-length (lst)
  (if lst
      (+ 1 (my-length (cdr lst)))
      0))Code language: Lisp (lisp)

この if lst は、「lst が空でなければ続ける、空なら終わる」という意味です。
空リストと偽が一致しているため、終了条件がとても短く書けます。

4.1. 条件分岐とデータ表現がつながる

Common Lisp では、「見つからなければ nil」「空なら nil」「偽なら nil」のように、条件分岐にそのままつながる形で値が返ることが多いです。

たとえば member は、見つかればその位置以降のリストを返し、見つからなければ nil を返します。

(member 3 '(1 2 3 4 5))
;=> (3 4 5)

(member 9 '(1 2 3 4 5))
;=> NILCode language: Lisp (lisp)

これをそのまま条件式に使えます。

(if (member 3 '(1 2 3 4 5))
    "found"
    "not found")
;=> "found"Code language: Lisp (lisp)

つまり、「真偽値専用の値だけを返す」のではなく、「役に立つ値を返しつつ、失敗時は nil にする」という設計が多いわけです。

4.2. 値がなければ既定値を選べる

or は最初に真とみなされた値を返します。
そのため、「値があればそれを使い、なければ既定値を使う」という形が書けます。

(defun display-name (nickname)
  (or nickname "anonymous"))

(display-name nil)
;=> "anonymous"Code language: Lisp (lisp)

5. nil が紛らわしくなる場面

便利な反面、nil にいろいろな役割が集まることで、意味の区別が必要になることがあります。

5.1. 「偽」と「空」と「値がない」が近い

たとえば関数が nil を返したとき、それが次のどれなのかは文脈によります。

  • 条件が偽だった
  • 空のリストだった
  • 検索に失敗した
  • 値がなかった

このため、返り値の意味は関数名やドキュメントで補う必要があります。

5.2. nil が「失敗」を表すとは限らない

空の結果が自然な場面では、nil は単なる正常な値です。

したがって、nil を見てすぐ「失敗」扱いすることはできません。

(remove-if-not #'oddp '(2 4 6 8))
;=> NILCode language: Lisp (lisp)

これは失敗ではなく、「条件に合う要素がなかった」という正常な結果です。

5.3. 区別が必要なら nil だけに頼れない

たとえば、「見つからなかった」のか、「見つかった値そのものが nil だった」のかを区別したいことがあります。
そういう場面では、nil だけに頼ると情報が足りません。

複数値や補助フラグを返す、あるいは別のシンボルを使う方が安全です。

(defun lookup-with-flag (key alist)
  (let ((pair (assoc key alist)))
    (if pair
        (values (cdr pair) t)
        (values nil nil))))Code language: Lisp (lisp)

6. まとめ

Common Lisp の nil は、「偽」であり、「空リスト」でもある特別な値です。
t は真を表す代表値ですが、空リストではなくアトムです。
そして、空でないリストはすべて真になります。

この設計によって、条件分岐とリスト処理がとても自然につながります。
その一方で、「偽」「空」「値がない」を nil に寄せるため、文脈によって意味を読み分ける必要があります。

つまり nil は、単なる false の置き換えではありません。
Lisp のデータ表現と制御構造を結びつける、設計の中心にある値なのです。