【Common Lisp】
Booleanと論理演算の基本の使い方
(t / nil / and / or / not)

  • Common Lispではnilだけが偽で、それ以外はすべて真として扱われます。
  • 型としてのbooleantnilの2つだけで、declareで明示すると値が真偽値に正規化されていることを示せます。
  • andorは真偽値ではなく評価した値そのものを返し、短絡評価により不要な式の評価を省略します。
  • bitは 0 と 1 の整数でありbooleanとは別の型です。

関連記事

1. 真偽値と boolean 型

Common Lisp では、条件式において nil だけが偽です。

一方、真として扱われるのは、t だけでなく、数値の 0 や空文字列 "" を含めたnil 以外のすべての値です。
これは generalized boolean(一般化真偽値)という考え方です。

一方で、型としての boolean はもっと狭く、tnil の2つだけです。

1.1. boolean型 は symbol のサブタイプ

boolean 型は symbol 型のサブタイプです。

tnil は、どちらもシンボルで、 boolean はその2つだけからなる集合として定義されています。

(symbolp t)   ;=> T
(symbolp nil) ;=> TCode language: Lisp (lisp)

Rust や Swift の列挙型に近い感覚で、シンボルそのものが値です。
ただし、C言語の enum のような整数との対応はありません。

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

nil には特別な性質があり、シンボルであるだけでなく、空リストでもあります。

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

nil は「偽」「空リスト」「空型の名前」など複数の役割を持ち、言語仕様(HyperSpec)でも nil は空リストとして記法 '() と交換可能だと説明されています。

nil は空リストなので、リスト処理と条件分岐が自然につながります。

(defun safe-first (lst)
  (if lst
      (car lst)
      nil))

(safe-first '(10 20 30))
;=> 10

(safe-first nil)
;=> NILCode language: Lisp (lisp)

(if (> (length lst) 0) ...) などとしなくても、 lst を条件式に入れるだけで、空リストは偽になります。

1.3. consp と組み合わせる

空でないリストかどうかを調べるには consp を使います。

(defun non-empty-list-p (x)
  (consp x))

(non-empty-list-p '(1 2))
;=> T

(non-empty-list-p nil)
;=> NILCode language: Lisp (lisp)

空リストは、コンスセルではなく、空でないリストは1つ以上のコンスセルで構成されます。

2. if / when / unless による分岐

2.1. if で分岐する

条件分岐の基本形は、if です。

(if 条件式 真の返り値 偽の返り値)Code language: Lisp (lisp)

if は、条件が真なら第2引数を、偽なら第3引数を評価します。

(defun pass-or-fail (score)
  (if (>= score 60)
      'pass
      'fail))

(pass-or-fail 80)
;=> PASS

(pass-or-fail 50)
;=> FAILCode language: Lisp (lisp)

2.2. cond で条件による場合分け

条件による場合分けでは cond を使います。

(cond (条件式 返り値) ...)Code language: Lisp (lisp)
(defun grade (score)
  (cond ((>= score 90) 'a)
        ((>= score 80) 'b)
        ((>= score 70) 'c)
        (t 'd)))

(grade 85)
;=> BCode language: Lisp (lisp)

2.3. when で真のときだけ実行する

真のときだけ処理したいなら when を使います。
複数式を書けます。

(defun print-if-positive (x)
  (when (> x 0)
    (format t "positive: ~a~%" x)
    x))

(print-if-positive 3)
; positive: 3
;=> 3Code language: Lisp (lisp)

返す値は、最後に評価した式です。
条件式が nil なら nil を返します。

2.4. unless で偽のときだけ実行する

unlesswhen の逆で、条件が偽のときだけ本体を実行します。

(defun ensure-name (name)
  (unless name
    (error "name is required"))
  name)Code language: Lisp (lisp)

条件式が nil でないなら、その値をそのまま返します。

3. 比較演算と真偽値

3.1. 数値比較

数値比較の結果は、条件式で使える真偽値です。

(defun in-range-p (x lo hi)
  (<= lo x hi))

(in-range-p 5 1 10)
;=> TCode language: Lisp (lisp)

3.2. eql / equal / equalp

オブジェクト比較でも真偽値が返ります。

(list (eql 10 10)
      (equal '(1 2) '(1 2))
      (equalp #(1 2 3) #(1.0 2.0 3.0)))
;=> (T T T)Code language: Lisp (lisp)

equalp は数値型や文字大小の差をゆるく扱うため、広めの一致判定に向きます。

3.3. nullnil を判定する

null は、引数が nil なら真を返します。

nil 判定を明示したいときによく使います。

(defun empty-list-p (x)
  (null x))

(empty-list-p nil)
;=> T

(empty-list-p '(1 2 3))
;=> NILCode language: Lisp (lisp)

4. 論理演算と短絡評価

4.1. not で真偽を反転する

not は引数が nil なら t を返し、それ以外なら nil を返します。
CLtL2 でもこの意味で定義されています。

(defun missing-name-p (name)
  (not name))

(missing-name-p nil)
;=> T

(missing-name-p "Alice")
;=> NILCode language: Lisp (lisp)

つまり、notは、nullの判定と同じ結果を返します。

4.2. and で条件をまとめる

andor は条件付き評価をする制御構造として説明されています。

and は左から順に評価し、途中で nil が出たらそこで止まります。

(defun valid-score-p (x)
  (and (integerp x)
       (<= 0 x)
       (<= x 100)))

(valid-score-p 80)
;=> T

(valid-score-p 120)
;=> NILCode language: Lisp (lisp)

and は常に t を返すわけではありません。
全部真なら最後の値を返します。

(and 10 20 30)
;=> 30

(and 10 nil 30)
;=> NILCode language: Lisp (lisp)

4.3. or で代替値を選ぶ

or は左から順に評価し、最初に真だった値を返します。
すべて偽なら nil です。

(defun user-name (nickname full-name)
  (or nickname full-name "guest"))

(user-name nil "Alice")
;=> "Alice"

(user-name nil nil)
;=> "guest"Code language: Lisp (lisp)

こちらも、返り値は必ずしも boolean ではありません。

(or nil nil 42)
;=> 42Code language: Lisp (lisp)

4.4. 短絡評価と制御

andor は必要なところまでしか評価しません。

これを短絡評価(short-circuit)といいます。
たとえば、x0 のときは、後半の (/ 1 x) まで進まないため、ゼロ除算を避けられます。

(defun safe-reciprocal (x)
  (and (not (zerop x))
       (/ 1 x)))

(safe-reciprocal 2)
;=> 1/2

(safe-reciprocal 0)
;=> NILCode language: Lisp (lisp)

つまり、andは、引数の数の違いはありますが、真のときに次の式を返すので、whenに似ています。
andは、すべての式で短絡評価があり、whenははじめの式だけで短絡評価があります。

4.5. 述語関数では t / nil にそろえる慣習

述語(predicate、真偽を返す関数)として定義する関数は、戻り値を t / nil にそろえるのが一般的です。

このような関数は、慣習的に -p(あるいは-?)という接尾辞を付けます。

(defun non-empty-string-p (x)
  (declare (type t x))
  (and (stringp x)
       (> (length x) 0)
       t))

(non-empty-string-p "abc")
;=> T

(non-empty-string-p "")
;=> NILCode language: Lisp (lisp)

途中の and だけだと最後の式の値が返るため、最後に t を置けば boolean に正規化できます。

5. declareboolean

5.1. declare (type boolean ...) の意味

変数や戻り値が boolean 型であることは、declare で明示できます。

ここでいう booleantnil だけです。

(defun even-boolean-p (x)
  (declare (type integer x))
  (let ((result (evenp x)))
    (declare (type boolean result))
    result))Code language: Lisp (lisp)

この宣言は、条件式で使える値全般ではなく、 tnil しか入らない前提になります。

一方、多くの条件式では「真なら何か値を返す」で十分で、関数の返り値を必ずしも t / nil に正規化する必要はありません。

(defun find-user (name table)
  (find name table :key #'car :test #'string=))Code language: Lisp (lisp)

この find-user は、見つかれば要素を返し、見つからなければ nil を返します。
条件式にはそのまま使えますが、戻り値は boolean型 と宣言することはできません。

(if (find-user "Alice" '(("Bob" . 1) ("Alice" . 2)))
    'found
    'missing)
;=> FOUNDCode language: Lisp (lisp)

5.2. booleanbit の違い

booleanbit は似ていますが、型としては別です。

bit 型は、0/1 のフラグ列を省メモリで持ちたいときに使います。

bit01 の整数です。
bit-vector は、その bit を要素に持つベクタです。

(defun make-flags (n)
  (make-array n :element-type 'bit :initial-element 0))

(make-flags 8)
;=> #*00000000Code language: Lisp (lisp)
(defun enable-flag (flags i)
  (setf (aref flags i) 1)
  flags)

(enable-flag (make-flags 8) 3)
;=> #*00010000Code language: Lisp (lisp)

この 0 / 1 は論理値ではなく整数です。
条件分岐に使う真偽値とは役割が違います。

(list (typep t 'boolean)
      (typep nil 'boolean)
      (typep 1 'bit)
      (typep 0 'bit))
;=> (T T T T)Code language: Lisp (lisp)

しかし、tbit ではなく、1boolean ではありません。

(list (typep t 'bit)
      (typep 1 'boolean)
      (typep nil 'bit)
      (typep 0 'boolean))
;=> (NIL NIL NIL NIL)Code language: Lisp (lisp)

ここで重要なのは、0boolean 型ではなく、条件式では真であることです。

(if 0 :true :false)
;=> :TRUE

(typep 0 'boolean)
;=> NILCode language: Lisp (lisp)

6. まとめ

Common Lisp では、条件式に使う真偽と、型としての boolean を分けて考えるのが大事です。

条件式では nil だけが偽で、それ以外は真です。
これは generalized boolean です。
一方、boolean 型は tnil だけです。
declare (type boolean ...) を使うと、「この値は真偽値として正規化されている」という意図を表しやすくなります。

また、bit01 の整数であり、boolean とは別です。
条件分岐には boolean、大量のフラグ列には bit-vector という使い分けが基本になります。