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

  • Common Lisp では、偽は nil だけで、それ以外は真です。
  • boolean 型は tnil の2つだけを表します。
    一方、条件式では generalized boolean(一般化真偽値、nil が偽でそれ以外が真)を使います。
  • ifwhenunless で真偽による分岐を書けます。
  • andor は単なる真偽値ではなく、評価した値そのものを返すことがあります。
  • declareboolean 型を宣言すると、「この値は tnil で返す」という意図を明確にできます。
  • bit01 の整数であり、boolean とは別物です。
    ビットベクタやビット演算で使います。

関連記事

1. Boolean の基本

Common Lisp では、条件式において nil だけが偽です。
nil 以外の値は、数値の 0 や空文字列 "" を含めて、すべて真として扱われます。
これは generalized boolean(一般化真偽値)という考え方です。

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

1.1. tnil

boolean 型の代表的な値は tnil です。

(list t nil)
;=> (T NIL)Code language: Lisp (lisp)

nil には特別な性質があり、偽であるだけでなく、空リストでもあります。
HyperSpec でも nil は「偽」「空リスト」「空型の名前」など複数の役割を持つと説明されています。

(list nil '())
;=> (NIL NIL)Code language: Lisp (lisp)

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

2.1. if で分岐する

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

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 を使います。

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

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)

5. 短絡評価

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)

5.1. and は、前提条件をまとめる

(defun readable-file-p (path)
  (and path
       (probe-file path)
       t))Code language: Lisp (lisp)

最後に t を付けることで、見つかった pathname ではなく boolean を返す形にしています。

5.2. or は、デフォルト値を与える

(defun getenv-or-default (value default)
  (or value default))

(getenv-or-default nil "guest")
;=> "guest"Code language: Lisp (lisp)

6. declareboolean

6.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 が入る想定で書きます。

6.2. generalized boolean との違い

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

これを、generalized-boolean 型といいます。

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

6.3. 述語関数では 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 に正規化しています。

6.4. 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)

7. nil とリストの関係

nil は偽であると同時に空リストでもあります。

言語仕様(HyperSpec)でも nil は空リストとして記法 () と交換可能だと説明されています。
このため、リスト処理と条件分岐が自然につながります。

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

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

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

7.1. consp と組み合わせる

リストが空でない cons かどうかを調べるには 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)

consp の返り値は generalized boolean です。

8. まとめ

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

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

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