第5章「データと制御フロー」

概要

ANSI Common Lispの第5章で定められている関数やマクロなどを以下の分類に合わせて個別に説明します。

定義

この節では「定義」に関するオペレータを扱います。

変数と定数の定義: defvar, defparameter, defconstant

シンボルを値として定義するオペレータは3つあり、全てマクロとして定義されています。
Macro: defvar
シンボルを変数として定義します。一度定義されている変数は再定義しません。
Macro: defparameter
シンボルを変数として定義します。一度定義されているか否かに関わらず定義します。
Macro: defconstant
シンボルを定数として定義します。少々使いづらいため、実際はあまり使われません。
defvarは一度定義されたシンボルか否かをチェックしてから定義するかどうかを決めるため、defparameterよりもワンステップ多く処理が発生します。その代わり、不用意な二重定義が発生しにくくなります。最も、defvarで再定義に遭遇しても警告が出る訳ではなく、何もしないだけなので、名前の衝突には十分気をつけてください。

また、Common Lispのコーディングスタイルでは、トップレベルの変数は*という記号で囲み、定数は+で囲むという文化があります。多くのパッケージはこの暗黙のルールに従っていますので、他人にコードを見せる場合は暗黙のルールを守りましょう。
defvardefparameterは「パラメータ」として使うこともできます。トップレベルの値をletで変更した場合は、letを抜けたら値が元に戻るため、一時的な値の変更を伴う処理に使うことができます。

再定義の扱いの違いとパラメータとしての使用を絡めたサンプルを以下に示します。
(defvar *v1* 10
  "*v1*: test variable 1st.")    ; ドキュメントも含めることができる
; => *V1*
(defparameter *v2* 20
  "*v2*: test variable 2nd.")
; => *V2*

(defvar *v1* 30)        ; 再定義は無効だがエラーは起きない
; => *V1*
(defparameter *v2* 40) ; 再定義可能
; => *V2*

*v1*
; => 10                ; 再定義が無視されている
*v2*
; => 40                ; 再定義されている

(let ((*v1* 50)
      (*v2* 60))
  (format t "*v1* is ~a.~%" *v1*)
  (format t "*v2* is ~a.~%" *v2*)) 
; *v1* is 50.
; *v2* is 60.          ; どちらも値が一時的に変更されている

*v1*
; => 10                ; 値が元に戻っている
*v2*
; => 40

関数の定義: defun

関数を定義するにはdefunを使います。関数の定義には各種宣言やドキュメントを含めることができます。

例えば、数値計算を行うプログラムでベクトルの要素毎の加算を行いたい場合、以下のような関数v+を定義しておくと便利です。
(defun v+ (vector1 vector2)
  (declare (type (vector double-float *) vector1 vector2)    ; 型の宣言
           (optimize speed))                                 ; 最適化宣言
  "Function [v+]: Plus the elements of the two vectors.
For performance, the element type must be 'double-float."    ; ドキュメント
  (map '(vector double-float *) #'+ vector1 vector2))        ; 関数本体
; => V+

(v+ #(1.1d0 2.2d0 3.3d0) #(10.0d0 20.0d0 30.0d0))
; => #(11.1d0 22.2d0 33.3d0)

(documentation #'v+ t)                                       ; ドキュメントの呼び出し
; "Function [v+]: Plus the elements of the two vectors.
; For performance, the element type must be 'double-float."

それほど利用頻度は多くないと思いますが、defunの仕様として定められている他の機能を2つ紹介します。

一つは「脱出」です。関数の内部では暗黙のうちにblockで括られ、関数名がblockの名前として使われます。そのため、関数内部の任意の場所でreturn-fromを使うことができます。
もう一つはsetfの扱いです。ANSI Common Lispのdefunsetfに対応できるように拡張されており、setfの第1引数で用いるplaceを関数として定義することができます。例えば、リストまたはベクトルの中央の値を書き換えるプログラムは以下のように定義できます。
; アクセッサ関数(Getter)
(defun middle (seq)
  (multiple-value-bind (q r)
      (truncate (length seq) 2)
    (if (zerop r)
        (elt seq (1- q))
        (elt seq q))))
; => MIDDLE

; setf用のアクセッサ関数(Setter)
(defun (setf middle) (value seq)
  (multiple-value-bind (q r)
      (truncate (length seq) 2)
    (if (zerop r)
        (setf (elt seq (1- q)) value)
        (setf (elt seq q) value))))
; => (SETF MIDDLE)

(defparameter *v1* #(1.5d0 2.3d0 3.7d0 4.9d0))
; => *V1*

; Getterを使用する
(middle *v1*)
; => 2.3d0

; Setterを使用する
(setf (middle *v1*) 0.0d0)
; => 0.0d0

; 変更されている
*v1*
; => #(1.5d0 0.0d0 3.7d0 4.9d0)
(setf function-name)という名前の関数を定義することは違和感があるかもしれませんが、ANSI Common LispではFunction nameは「シンボルまたはsetfで始まる2要素のリスト」と決まっており、まさにこの形式自体が関数の名前として定義されます。

なお、上記ではGetterSetterを完全に分けて定義していますが、Setterの中でGetterの定義を利用して以下のように定義してしまうと無限ループになりますので気をつけてください。
; Bad Example!!
(defun (setf middle (value seq)
  (setf (middle seq) value)))
setfはこのような式に遭遇すると
(funcall #'(setf middle) value seq)
という形に展開して関数を呼び出すため、終了条件のない再帰呼出しとなります。

マクロの定義: defmacro

マクロを定義するにはdefmacroを使います。

Common Lispのようなマクロは他のいかなる言語にも用意されていない強力な抽象化技法です。マクロはソースコードの変形であり、新しい構文を導入したり、ソースコードの節約に寄与したりすることができます。その役割は関数とは異なっていて、全てのマクロは最終的な展開を終えると関数とスペシャルオペレータの集合に還元されます。つまり、関数は値を返しますが、マクロはソースコードを返すのです。

Common Lispの標準で定められている機能の中でもマクロは重要な役割を担っており、次章で扱う繰り返しは全てマクロで定義されていますし、Common Lispでクラスや構造体を扱う場合もほとんどがマクロの処理で構築されています。

ここでは、マクロを使って新しい演算子(構文)を定義する例を示します。Common LispをはじめとするLispは前置記法を取るため、数式などの記述が深くネストしやすく、とても読みにくくなってしまいます。そこで、マクロを使って計算を上から下へ記述できるようにする「アロー(arrow)演算子->」を導入します。
(defmacro -> (&rest rest)
  (reduce #'(lambda (v x)
              (if (atom x)
                  `(,x ,v)
                  `(,(car x) ,v ,@(cdr x))))
          rest))
; ->

(-> 1
    (+ 2)
    (* 3)
    (/ 4)
    print)
; 9/4
; => 9/4

(macroexpand-1 '(-> 1
                    (+ 2)
                    (* 3)
                    (/ 4)
                    print))
; => (PRINT (/ (* (+ 1 2) 3) 4)) ; T

手前の式を次の式の第1引数にしていくマクロです。式がリストではない場合も自動でリストにして関数適用の形式に変更します。非常に簡素な定義ですが、長い数式を記述する場合などは便利です。なお、例にある通り、マクロを確認したい場合はmacroexpand-1を用いると展開することができます。SLIMEを用いて開発している場合は C-c M-m で展開することもできます。

ちなみに、defmacroのラムダリストでは、destructuring-bindで用いるような構造化代入を使うことができます。例えば、ファイルへの書き込みを行うマクロは以下のように定義できます。
(defmacro write-file ((out file) &body body)
  `(with-open-file (,out ,file :direction :output 
                               :if-exists :supersede)
     ,@body))
; => WRITE-FILE

; カッコの中にストリームとファイル名を入れ、
; カッコの外に書き込みを行うbody本体を記述すると見やすい。
; カッコがあってもマクロの分配機能で簡単に記述できる(上の定義参照)。
(write-file (out "test.txt")
  (write-line "Hello, World" out))
; => "Hello, World"


なお、手前の式を次の式の最後の引数として渡す「ダブルアロー演算子(->>)」は以下の通りです。これらのアロー系演算子はUNIXのパイプ演算子やR言語の拡張である%>%などにもあります。アローの記号を使っているのはCommon Lispと同じLISP系言語のClojureです。

<ダブルアロー演算子の定義例>
(defmacro ->> (&rest rest)
  (reduce #'(lambda (v x)
              (if (atom x)
                  `(,x ,v)
                  `(,@x ,v)))
          rest))

束縛

この節では束縛( bind )に関するオペレータを説明します。

レキシカルな束縛: let, let*

ローカルな変数を使用するにはlet及びlet*を使用します。
Special Operator: let
並列的にレキシカルな変数を使用します。手前の変数の束縛は次の変数の束縛では参照できません。
Special Operator: let*
直線的にレキシカルな変数を使用します。手前の変数の束縛は次の変数の束縛で参照できます。
並列的と直線的とは、以下のような違いです。
; 並列的な束縛
; b の束縛では a を使用しないので、a と b を同時に束縛できる
(let ((a 10)
      (b 20))
  ...)

; 直線的な束縛
; b の束縛で a を使用するので、a と b は順番に束縛する必要がある
(let* ((a 10)
       (b (+ a 10)))
  ...)

; let で直線的に束縛する
; 可能だがネストが深くなる
(let ((a 10))
  (let ((b (+ a 10)))
    ...))

また、letlet*は「レキシカル変数」である点に注意が必要です。これは第3章でも説明しましたが、スペシャル変数とは明確に区別されます。

スペシャル変数はシンボル自体に意味があるので、束縛された値がsymbol-value関数で参照できます。
(defvar *a* 10)
; => *A*
(symbol-value '*a*)
; => 10

しかし、letlet*で束縛されるのはレキシカル変数であるため、シンボル自体に意味は無く、symbol-value関数では参照できません。これは、名前の衝突を防ぐため、letlet*で束縛されるシンボルは実際の束縛の前にgensym関数などを用いた無作為なシンボルに置き換えられるためです。レキシカル変数のsymbol-valueを参照するとエラーになります。
(let ((a 10))
  (symbol-value 'a))
; SYMBOL-VALUE: variable A has no value
;    [Condition of type SYSTEM::SIMPLE-UNBOUND-VARIABLE]

ただし、スペシャル変数として宣言すればレキシカル変数ではなくスペシャル変数として扱うこともできます。
(let ((a 10))
  (declare (special a))
  (symbol-value 'a))
; => 10

このようなスペシャル変数はトップレベルの変数ではないためletの外では参照できません。

スペシャルな束縛: progv

ほとんど使うことはないと思いますが、special宣言付きのletを使う代わりにprogvというスペシャルオペレータも用意されています。
(progv '(*b*) '(20)
  (symbol-value '*b*))
; => 20
progvスペシャルオペレータはletlet*とは記述のスタイルが異なっており、第1引数は束縛するシンボルのリスト、第2引数が束縛する値のリスト、第3引数以降が body になります。

このprogvもトップレベルの変数束縛ではないので、progvの外では参照できません。

多値の束縛と構造化代入: multiple-value-bind, destructuring-bind

多値と構造化代入は第3章「評価とコンパイル」の「多値」でサンプルを含めて説明しているので、そちらを参照してください。

関数の束縛: flet, labels

letlet*は値をシンボルに束縛しますが、fletlabelsは関数をシンボルに束縛します。
Special Operator: flet
ローカルな関数を定義します。関数定義の内部で定義している関数を呼び出すこと(再帰)はできません
Special Operator: labels
ローカルな関数を定義します。再帰も可能です。
defunで定義される関数も定義内で自らを呼び出す「再帰呼出し」に対応しているため、defunのローカル版というとlabelsになります。以下ではリストのリストにおける要素の和を求めていますが、labelsで定義されている内部関数sum%の中で再帰呼出しを用いています。
(defun labels-test (list)
  (labels ((sum% (lst sum)
             (if (null lst)
                 sum
                 (sum% (cdr lst)                 ; 再帰呼出し 
                       (+ (reduce #'+ (car lst))
                          sum)))))
    (sum% list 0)))
; => LABELS-TEST

(labels-test '((1 2 3) (4 5 6) (7 8 9 10)))
; => 55

一般的にはlabelsでローカル関数を使いたいというニーズを満たすことができますが、追加でfletが用意されているのには大きく3つの理由があります。
  1. 再帰を用いない内部関数と再帰を用いる内部関数をfletlabelsで使い分けることでコードの可読性が向上する
  2. 再帰を用いない内部関数でfletを使用するとlabelsを使用する場合よりわずかに高速になる
  3. labelsでは定義できず、fletでは定義できる場合もわずかに存在する
3つ目の理由は詳しく知っておくのに値すると思うので、サンプルを考えてみました。
(defun flet-test (list)
  (flet ((+ (lst)
           (let ((x (first lst))
                 (y (second lst)))
             (format t "~a + ~a = ~a~%" x y (+ x y)))))
    (mapc #'+ list)))
; => FLET-TEST

(flet-test '((1 2) (3 4) (5 6)))
; 1 + 2 = 3
; 3 + 4 = 7
; 5 + 6 = 11
; => ((1 2) (3 4) (5 6))

「再帰に使えない」というのはネガティブな捉え方であり、ポジティブに捉えれば「定義内で自分と同じ名前の関数呼出しがあっても、それは別の関数だと考える」と解釈することもできます。そこでここでは一般の(COMMON-LISPパッケージの)+関数を使用して別のローカルな+関数をfletで定義しています。これもリストのリストを引数にとりますが、足し算の表示を行いながら処理を進めます。(mapc #'+ ...)で使われている#'+fletで定義した内部関数ですが、(+ x y)で使われている+COMMON-LISPパッケージの標準的な加算関数です。labelsで定義するとこれが再帰呼出しとして解釈されるため、終了条件のない無限ループに陥ることになります。

このように、数は少ないと思いますがfletでしか定義できない場合もありますので、再帰を用いない場合はfletを使うように分けておきましょう。

マクロの束縛: macrolet

変数、関数と来たので、次はマクロの束縛です。ローカルなマクロを定義するにはmacroletを使います。

このmacroletlet / let*flet / labelsと異なり、関数の内部ではなくトップレベルで使うことがよくあります。マクロは強力な抽象化技法なので「関数の定義」を行うマクロも簡単に作れますが、macroletの内部で関数が定義されるとそれはトップレベルでの関数定義と等しくなるため、マクロの力を使って関数を定義しつつ、関数の定義に使ったマクロはローカル環境を離れると破棄するという潔い使い方ができるのです。その場でしか使わないようなマクロというのは結構多くありますが、defmacroで定義するとマクロが残ってしまうので、マクロの使用を躊躇しがちです。macroletを使うとそのような躊躇をせずにその場限り(あるいはその場しのぎ)のマクロを堂々と使うことができます。
macroletを使ってその場限りのマクロを定義し、そのマクロで関数を素早く定義するというサンプルを以下に示します。double-float型の値を持つベクトルの要素同士の四則演算を行う4つの関数v+, v-, v*, v/を定義しますが、関数定義の形がどれもそっくりになるので、macroletで関数定義のテンプレートを作ってから、マクロの適用で関数を定義します。
(macrolet ((defmap (name fn)
             `(defun ,name (x y)
                (declare (type (vector double-float *) x y)
                         (optimize speed))
                (map '(vector double-float *) ,fn x y))))
  (defmap v+ #'+)    ; Function: V+
  (defmap v- #'-)    ; Function: V-
  (defmap v* #'*)    ; Function: V*
  (defmap v/ #'/))   ; Function: V/
; => V/

(let ((a #(1.5d0 2.5d0 3.5d0))
      (b #(4.5d0 5.5d0 6.5d0)))
  (flet ((print-test (function value)
           (format t "~a: ~a~%" function value)))
    (print-test "V+" (v+ a b))
    (print-test "V-" (v- a b))
    (print-test "V*" (v* a b))
    (print-test "V/" (v/ a b))))
; V+: #(6.0d0 8.0d0 10.0d0)
; V-: #(-3.0d0 -3.0d0 -3.0d0)
; V*: #(6.75d0 13.75d0 22.75d0)
; V/: #(0.3333333333333333d0 0.45454545454545453d0 0.5384615384615384d0)
; => NIL

トップレベルではdefunが現れていないように見えますが、macroletの評価としてトップレベル上にprognで囲まれた4つのdefunが出現するため、4つの関数は広域で使用可能になります。

それぞれ定義される関数はとても小さな関数ですが、宣言をつけているので1つにつき4行で、一般には合計16行+関数間の空行3行=19行の定義になるはずです。macroletを使うと上記の通り9行で4つ分が定義できているので、半分以下の行数に短縮されています。と同時に、ここで使っているdefmapマクロは他では使うことはなさそうですから、macroletのようにその場限りのマクロとすることで、他の箇所に影響を与えずに済みます。
macroletはこのような使い方をすることがあり、必ずしも関数内部で使うだけではありませんので、記憶の隅に留めておいてください。

値の変更: setq, psetq

defvarletはシンボルを変数として利用できるようにしますが、一度定義されたシンボルの束縛を変更する目的では使えません。変数を書き換えるにはsetqpsetqを使用します。
Special Operator: setq
変数の束縛を変更します。複数の変数を同時に変更する場合はlet*のように直線的な扱いになります。
Macro: psetq
setqと同様に変数の変更を行いますが、letのように並列的な変更になります。
setqpsetqはいずれもシンボルと式を「ペア」として複数列挙することができますが、その扱いが異なります。一般的にはsetqのように直線的な変更が便利だと思いますが、並列的な値の変更が便利な場合もあります。例えば、二つの変数の値を入れ替える場合、直線的な変更であればどちらか片方の値を一時的な変数に束縛してから上書きしなければなりませんが、並列的な変更であればそのまま記述することができます。
(let ((a 1)
      (b 2))
  (psetq a b
         b a)
  (values a b))
; => 2; 1

ただし、ANSI Common Lispでは値の変更は次節のsetfで統一的に処理する扱いが一般的なので、特に意図的に使用したい場合以外はsetfを使う方が望ましいでしょう。なお、変更するのが単純にシンボルの束縛ならsetfsetqと等価なので、よりシンプルなsetqを使用するという人もいます。どちらを使うかは適宜決めてください。

汎変数: setf, psetf

関数の定義を行うdefunの節でも少し説明してしましたが、ANSI Common Lispでは仕様でsetfという極めて高機能なマクロが準備されています。これはあらゆる「代入操作」を統一的に扱えるようにしたsetqの拡張版で、すでに見てきたようにベクトル(配列)やクラスのスロットに対する代入操作もsetfを基盤に構築されています。
setfの扱いについては非常に膨大なトピックがあるので、この Spec Guide とは別のコンテンツで紹介したいと思っています。
setqの第1引数はシンボルですが、setfの第1引数は place です。place とはこのsetfのための概念であり、「汎参照として使うのに適したフォーム」です。汎参照とは「オブジェクトが保存されている場所への参照」であり、リストのcarや配列のaref、クラススロットのslot-valueなどが「汎参照」の一つです。もちろん、変数として使用(束縛)しているシンボルも汎参照です。
setfはこのような「汎参照」に対して値を束縛する「汎変数」として使われます。
setfcararefなどのアクセッサーで値の代入を行うように見えるものですが、実際にアクセッサーで代入している訳ではなく、アクセッサーの情報から事前に定められた代入用関数へとマクロ展開して、その関数で代入を行なっています。実際の代入用関数はmacroexpand-1などで見ることができます。
;; リストのcar
(macroexpand-1 '(setf (car list) 10))
; => (SYSTEM::%RPLACA LIST 10); T     ; 代入用関数は system::%rplaca

;; simple-vector
(macroexpand-1 '(setf (svref vector 0) 10))
; => (SYSTEM::SVSTORE VECTOR 0 10); T   ; 代入用関数は system::svstore

;; クラスのスロット
(macroexpand-1 '(setf (slot-value slot 'name) 10))
; => (CLOS::SET-SLOT-VALUE SLOT 'NAME 10); T
;    ; 代入用関数は clos::set-slot-value

なお、setfの展開で用いる関数などはget-setf-expansion関数を用いることで確認することができます。
;; 配列の setf をマクロ展開してみる
(macroexpand-1 '(setf (aref array 1 2) 10))
; => (LET* ((#:G3751 ARRAY)
;           (#:G3752 1)
;           (#:G3753 2)
;           (#:G3754 10))
;      (SYSTEM::STORE #:G3751 #:G3752 #:G3753 #:G3754))
;    T

;; place 部分を get-setf-expansion してみる
(get-setf-expansion '(aref array 1 2))
; => (#:G3755 #:G3756 #:G3757)
;    (ARRAY 1 2)
;    (#:G3758)
;    (SYSTEM::STORE #:G3755 #:G3756 #:G3757 #:G3758)
;    (AREF #:G3755 #:G3756 #:G3757)
get-setf-expansion関数は5つの値を多値として返します。それぞれ以下の意味を持ちます。
  1. vars : gensymされたシンボル
  2. vals : シンボルに束縛される値で、(setf ...)関数の引数
  3. store-vars : 新しい値を place にセットする際のシンボル
  4. writer-form : 代入用の関数
  5. reader-form : アクセス用の関数
setfの展開形を自分で定義して新しい place を作ることも可能ですが、その際の「代入用の関数」は実装毎に異なるため、このget-setf-expansionで得た値を使って定義することが一般的です。ただし、ANSI Common Lispで用意されている様々なデータ構造のアクセス用関数は全てsetfでも使用できるように(setf ...)関数が準備されているため、自分でsetfの展開形を書くことはほとんどないでしょう。もしそのように独自の place を定義する場合は適切なドキュメントを合わせて残しておくべきです。

なお、psetfpsetqsetf版で、並列的に値の変更を行います。

破壊的変更: shiftf, rotatef

setfマクロの基盤の上に構築されたちょっと変わったマクロを2つ紹介します。ちょっと変わった、と言ってもいずれもANSI Common Lispで標準化されたマクロです。

一つはshiftfマクロです。これは複数の place を引数に取り、その place を一つずつ「ずらす」というマクロです。最も直感的には、以下のサンプルで動作が分かると思います。
(let ((a 1)
      (b 2)
      (c 3)
      (new 4))
  (shiftf a b c new)
  (values a b c))
; => 2; 3; 4

このshiftfはマクロなので、単純に値をずらすのではなく、値をずらして元の順番でシンボルに再束縛するというものです。最初の引数の値はshiftfマクロの返り値となって返されますが、ここでは受け取っていないので1は破棄されています。

このshiftfマクロがsetfマクロの上に構築されているということは、以下のようにシンボルではなく place を使っても全く同じように「ずらす」ことができる、ということになります。
(let ((a '(1 2 3))
      (b '(4 5 6))
      (c '(7 8 9))
      (new '(10 11 12)))
  (shiftf (second a)
          (second b)
          (second c)
          (second new))
  (values a b c))
; => (1 5 3)
;    (4 8 6)
;    (7 11 9)

上の例ではsecondというアクセス用関数を使っているので、(setf second)関数が呼び出されますが、これは(system::%rplaca (cdr var) value)のような形式に展開され、リストの2番目の値を書き換えます。そのため、a, b, c それぞれのリストにおいて2番目の値だけがずれているのが分かると思います。

もう一つはrotatefです。これはshiftfと似ていますが、ずれた後の先頭の値が最後に回る点が異なります。
(let ((a 1)
      (b 2)
      (c 3))
  (rotatef a b c)
  (values a b c))
; => 2; 3; 1

当然ながら、rotatefする place が2つだけの場合、このページのpsetqで示した「入れ替え」をすることができますので、rotatefを使った方がシンプルで好ましいでしょう。

なお、rotatefマクロはshiftfマクロと異なり、ずれて破棄される値はないため、返り値は常にnilです。

適用

この節では関数の「適用」について説明します。

基本的な適用: apply

Common LispのようなLISP系言語では、関数に引数を与えて呼び出すことを「適用」と呼びます。Common LispにおいてS式の最初の要素はオペレータ、以降が引数ですから、「適用」とはS式の2番目以降の値を引数として、1番目の関数を呼び出すことを意味します。

具体例で示します。例えば、以下のような最もシンプルなS式があるとします。
(+ 1 2)
; => 3

上記の説明では、2番目以降を引数として、1番目の関数を呼び出すということなので、このS式は以下のような意味を持つことになります。
(apply #'+ '(1 2))
; => 3

関数を呼び出す(適用する)ための関数がapplyです。applyは最も基本的な高階関数の一つです。

このような関数適用の原則はS式一般に用いることができます。なぜならS式の1番目の要素とはcar部であり、2番目以降の要素とはcdr部ですから、より汎用性のある表現でapplyの動作を表すと以下のようになるでしょう。
(let ((s-exp '(+ 1 2)))
  (apply (car s-exp) (cdr s-exp)))
; => 3

ここではs-exp'(+ 1 2)というS式に束縛していますが、これは全てのS式に当てはまる関数適用の原則です。
apply関数はCommon Lispの関数適用の基礎そのものですが、実用面でも大いに役立ちます。引数が個々の値ではなくリストで渡されてくる場合も多いと思いますが、そのような時はapplyの出番です。(以下の例ではapplyではなくreduceでも同じ結果が得られます。)
(mapcar #'(lambda (list) (apply #'+ list))
        '((1 2 3) (4 5 6) (7 8 9)))
; => (6 15 24)

また、applyは高階関数なので、関数がデータとして存在する場合の適用にも用いることができます。以下のplus-n関数は指定した数を加算する関数を返り値として返しますが、返り値としての関数は常にデータの状態なので、applyなどの高階関数で適用することになります。
(defun plus-n (n)
  #'(lambda (x) (+ x n)))
; => ACM

(apply (plus-n 2000) '(17))
; => 2017

もっとも、上の例では引数の'(17)をリストにして渡しているように、リストの形ではない方が望ましい場合もあります。そのような時は次節のfuncall関数を使います。

なお、apply関数の第1引数として(car '(+ 1 2))が適用できることに違和感があった方もいるのではないでしょうか。(car '(+ 1 2))'+と等価ですから、(apply '+ '(1 2))という式になりますが、この時'+は関数ではなくシンボルなので高階関数として適用できないのではないか、と思うのは自然なことです。しかしANSI Common Lispの標準仕様では高階関数で引数として用いる関数( function )は厳密には関数指示子( function designator )として拡張されており、関数もしくは関数を表すシンボルとして定められています。そのため、シンボルが引数として渡されてきた場合はそのシンボルに関数が束縛されていないかどうかを確認し、関数が束縛されていればそれを使います。他方、#'+のように関数の実体を直接引数として渡したい場合はsymbol-function関数でシンボルから関数の実体を取り出せば同じように使うことができます。

symbol-function関数で関数の実体を取り出してapplyする例>
(let ((s-exp '(+ 1 2)))
  (apply (symbol-function (car s-exp)) (cdr s-exp)))
; => 3

リスト以外の適用: funcall

funcall関数がapply関数と異なるのは引数がリストではなく個別の値である点です。例えば、前節の例だと17をリストにする必要はありません。
(funcall (plus-n 2000) 17)
; => 2017
applyとの違いはこの点だけですが、applyにはfuncallのようにリストではない個別の値を適用できる裏技も実は用意されています。それは、引数の最後にnilを付け加えることです。
(apply (plus-n 2000) 17 nil)
; => 2017

リスト構造は第14章「リスト」で詳しく説明しますが、一番最後の要素(cdr部)が必ず空リスト(nil)になっています。そしてCommon Lispのソースコードはそれ自体がリストなので、このような動作になります。

何もしない関数: identity

関数とは通常、引数に対して何らかの計算を行い、その評価結果を返り値として返すのがその主な仕事ですが、時に「何もしない」関数が必要になる場合もあります。そのような何もしない関数はそのまま単独で使うことは無く、高階関数の引数として渡します。ANSI Common Lispでは、identity関数が引数をそのまま返り値として返すだけの「何もしない関数」です。

(mapcan #'identity '((1 2 3) (4 5 6) (7 8 9)))
; => (1 2 3 4 5 6 7 8 9)
mapcanは高階関数の一つで、第2引数以降のデータ(シーケンス)に対して第1引数(関数)を適用し、その評価結果がリストであるという前提で結果を連結していきます。今回はただ単に二重リストをフラットにしたいだけなので、第1引数の関数として期待される役割は無く、identityを渡しています。

複数の関数適用: progn

関数に対して引数を渡して適用し、その返り値をまた別の関数に適用し、という関数適用の連続だけで計算が終わるという場合、そのプログラムのS式は最終的には1つであると見なすことができます。それはCommon Lispの評価が「内側から」進んでいくため、内側から外側へと評価結果が渡るだけで計算できるからです。以下のような計算はこのような事例です。
(* (+ 1 2) 3)
; => 9

しかし、全ての計算が関数の評価と適用の連続だけで記述できるわけではありません。例えば、乱数を発生させ、その値を使って2つの計算を行う場合、必ず乱数の値をシンボルに束縛するという独立した式が必要になります。
(setq ran (random 100))
; => 85
(format t "~a + 100 = ~a~%" ran (+ ran 100))
; 85 + 100 = 185
; => NIL
(format t "~a * 100 = ~a~%" ran (* ran 100))
; 85 * 100 = 8500
; => NIL

一連の計算が複数のS式で記述される場合でもそれを一つのS式にまとめたい場合があります。例えばdefunの関数定義における body の部分や、letでレキシカル変数を束縛してその値を使って何か処理を行うような場合です。このように、一連の処理を1つにまとめたい場合、つまり複数の関数を順番に適用する場合はprognスペシャルオペレータを使います。

ここで重要なのは、最初に述べた通り、複数の関数を順番に適用する場合でも、それぞれの関数の評価結果(返り値)をつなぎ合わせて計算する場合、最終的には1つのS式として記述することができますから、prognスペシャルオペレータは不要になるということです。複数の関数を順番に適用しつつprognを使うことが必要な場合とは、「副作用」を伴う場合です。副作用とは、そのような関数適用の連続だけでは表せない処理、例えば変数の束縛や出力などがあります。
(setq fortune (random 2))
; => 1

(if (= fortune 1)
    (progn (setq fortune 'Good) "Lucky!!")
    (progn (setq fortune 'Bad) "Unlucky!!"))
; => "Lucky!!"

fortune
; => GOOD

上記は01かの乱数を使って占いを行なっています。次節で説明するifスペシャルオペレータは条件分岐を行いますが、1つのS式しか記述できないのでprognで囲っています。prognで囲まれた部分は最後の評価結果が全体の返り値となります。

実際にはprognで囲まなければならない場面はそう多くありません。それは、「暗黙的なprogn」が様々なオペレータで使われているためで、先に説明したletなども body の部分は暗黙的にprognされています。また、ifは1つのS式しか記述できませんが、condは暗黙的にprognで囲まれるため、複数の式を記述できます。

ちなみにdefunの定義( body )部分も同じようにprognで囲まれているように見えますが、実はdefunblockという別のスペシャルオペレータで暗黙的に囲まれています。こちらはブロック構造に名前を付けることができ、そのブロックから任意に脱出できます。(defunの節で説明した通りです。)

条件分岐

この節では「条件分岐」に関するオペレータを紹介します。

2方向条件分岐: if

ANSI Common Lispでは様々な条件分岐オペレータが用意されていますが、最も基本的なオペレータはifスペシャルオペレータです。これは条件に適合する場合と適合しない場合の2方向にシンプルに分岐する場合の処理に適しています。また、他の全ての条件分岐オペレータはifを基盤に構築されたマクロとして定義されています。
(if 0
    'zero
    'not-zero)
; => ZERO

直接的には第10章「シンボル」で扱いますが、ANSI Common Lispの標準で定められた真偽値はtnilです。ただし、nil以外の全ての値はtを継承したオブジェクトなので、tに合致し、「真」として扱われます。0""(空文字)なども同様です。ただし、'()(空リスト)だけはnilと同義として扱われます。
ifは非常にシンプルなオペレータで自由度も高いですが、ANSI Common Lispには前述の通り多様な条件分岐オペレータが用意されていることから、以下の条件を満たす場合にのみifを使うことが推奨されます。
  • 2方向条件分岐であること。1方向分岐の場合はwhenunlessを使用する。
  • 評価式が単一のS式であること。prognで複数の式を記述する場合はcondを使用する。

等価性判定述語: eq, eql, equal, equalp

ANSI Common Lispには「等しいかどうか」を判定する関数が用意されています。
Function: eq
2つの引数が同一のオブジェクトである場合にtとなります。主にシンボルの判定に使います。
Function: eql
eqtになるものに加え、同一の型の文字が等しい場合にtとなります。整数と小数を区別したい場合などに使用します。
Function: equal
シンボルの場合はeqと同じであり、数または文字の場合はeqlと同じです。リストや文字列の場合は各要素同士を比較比較してくれるので、文字列まで含めた比較に使用されます。
Function: equalp
構造体やハッシュテーブルなど、より複雑な複合データにも対応し、要素毎に比較してくれます。
これらの他にも個別のデータ型で述語が定められていますので、一般的な使い方をごく簡単に示します。
  1. 比較対象がシンボルだけの場合、eqを使用することで高速になります。
  2. 比較対象がシンボルに限定されず、文字列やリスト同士を比較する可能性がある場合、equalを使用します。
  3. 比較対象が数だけまたは文字列だけに限定されている場合、=またはstring=を使用します。型が決まっているので高速に処理されます。ただし、整数と小数は区別されず、数学的な意味で比較を行うため、(= 1 1.0)tになります。
  4. その他一般にオブジェクトの比較を行う場合はeqlを使用します。一部のオペレータを除き、デフォルトの述語はeqlが採用されています。
  5. 構造体の比較を行う場合などはequalpを使用しますが、速度が重要になる場合はequalpで気軽に比較するべきではありません。また、文字列の比較でequalpを使用すると大文字と小文字も区別されない点に注意してください。

論理オペレータ: and, or, not

条件判定における論理演算子として代表的なものが3つ用意されています。
Macro: and
論理積です。「かつ」を意味します。
Macro: or
論理和です。「または」を意味します。
Function: not
否定です。「ではない」を意味します。
andorは複数の判定式を記述できますが、andは途中でnilになった時、orは途中でtになった時に評価結果が確定して脱出するため、その後の式は評価されません。
(defparameter *temp* nil)
; => *TEMP*

(and (eql "Hello" "Hello")
     (setq *temp* t))
; => NIL

*temp*
; => NIL
eqlを使って文字列を比較するとnilになりますから、setqの式は評価されずに脱出します。
なお、参考までに論理積と論理和の違いを示しておきます。2つの式の結果がtnilの組み合わせの時にandorでどのような違いがあるかを示しています。

式1 式2 and or
t t t t(式2は評価しない)
t nilnilt(式2は評価しない)
nilt nil(式2は評価しない)t
nilnilnil(式2は評価しない)nil

脱出をうまく利用して高速化する場合は、厳しそうな条件を手前に持ってくる方が望ましいでしょう。

1方向条件分岐: when, unless

ifは2方向の条件分岐でしたが、whenunlessは1方向の条件分岐です。
Macro: when
条件式がtの時に bodyが順番に評価されます。
Macro: unless
条件式がnilの時に bodyが順番に処理されます。
ifの節で述べた通り、whenunlessで対応可能な1方向分岐の場合、こちらを優先して使いましょう。whenunlessbody が暗黙的にprognされるので、複数の式を記述することもできます。

例えば、数のリストに含まれる0の数と0以外の数を取得する関数をそれぞれ独立して定義する場合、以下のように記述できます。
(defun count-zero (list)
  (let ((sum 0))
    (mapc #'(lambda (v) (when (zerop v) (incf sum))) list)
    sum))
; => COUNT-ZERO

(defun count-not-zero (list)
  (let ((sum 0))
    (mapc #'(lambda (v) (unless (zerop v) (incf sum))) list)
    sum))
; => COUNT-NOT-ZERO

(let ((list '(1 3 0 7 0 6 4 8 0 9)))
  (format t "zero    : ~a elements.~%" (count-zero list))
  (format t "not-zero: ~a elements.~%" (count-not-zero list)))
; zero    : 3 elements.
; not-zero: 7 elements.
; => NIL

多方向条件分岐: cond

多くの言語に else if のような多方向の条件分岐があるように、ANSI Common Lispでも多方向の条件分岐としてcondマクロが用意されています。ちなみに、2条件分岐を行うifが言語に導入されたのはLISPが最初で、昔のFORTRANは1条件分岐しかなく、条件に合致したら GO で指定の行に制御を飛ばす、ということをしていました。LISPは論理的な記述を得意としていたので、条件分岐のためのオペレータが非常に多く用意されているのです。

例えば、シンボル、数、文字列のいずれかの型を判定し、それ以外は全て'unknownを返すような関数は多方向に分岐するため、condマクロで記述できます。
(defun simple-type-of (x)
  (cond ((typep x 'symbol) 'symbol)
        ((typep x 'number) 'number)
        ((typep x 'string) 'string)
        (t 'unknown)))
; => SIMPLE-TYPE-OF

(simple-type-of 'hello)
; => SYMBOL

(simple-type-of "Hello")
; => STRING

(simple-type-of '("Hello"))
; => UNKNOWN
condマクロではどこにも該当しない条件をキャッチするために、tを使用することができます。tだけを条件式の場所においておけば定数として常に真となり、必ず実行されます。ただし、手前の条件に該当した場合はcondマクロから抜けるため、最後に置いたtに到達するのはどの条件にも該当しなかった場合だけとなり、最終的な else の役目を果たすことになります。

ちなみに、型によって分岐させる場合は次の節で説明するtypecaseを使用するべきです。また、オブジェクト指向で記述する場合は、メソッドを利用することも可能です。

値や型による分岐: case, typecase

condマクロは大変便利ですが、条件判定が非常にシンプルな場合は、記述が冗長になります。その場合とは、単純に値や型によって分岐したい場合です。

例えば、三択問題で2番目の'bを選んだ場合に正解を示す関数を考えます。この場合、判定すべき対象は回答者が選んだ選択肢で、分岐は'a, 'b, 'cの三択のいずれかです。このような時、分岐は「条件」ではなく「値」によって行われることになります。条件であることを示すなら、「'aに等しい」などのようにeqを使って式を記述する必要がありますが、値なら'aなどをそのまま列挙すればいいことになります。
(defun good-b (x)
  (case x
    ((a c) 'bad)
    ((b) 'good)
    (t 'unknown)))
; => GOOD-B

(good-b 'c)
; => BAD

(good-b 'b)
; => GOOD
caseマクロは値による分岐に効果を発揮しますが、使用上の注意点がいくつかあります。
  1. 判定に用いる値は自動的にquoteされるため、シンボルでも'を付ける必要はありません。
  2. 値として文字列は使えません。これは等価性判定に用いる述語がeqlで固定なためです。
  3. t自体を値として用いたい場合は必ず(t)としてください。tだけを値にすると全てが該当してしまいます。
  4. 3の問題を緩和するため、caseでは最終的な else の役目としてtではなくotherwiseというキーワードを用いることが可能になっています。
typecaseマクロは型で分岐する場合に用います。前節のsymple-type-of関数をtypecaseで書き換えると以下のようになります。
(defun simple-type-of (x)
  (typecase x
    (symbol 'symbol)
    (number 'number)
    (string 'string)
    (otherwise 'unknown)))

特殊制御

この節では、直接使うことは非常に稀な特殊な制御オペレータについて説明します。

「制御」とは、プログラムの実行の順番を意図的に変更するような動作を意味します。例えば、関数は最後の式が評価されてその結果が呼出し元に戻りますが、最後の式に到達するまでに戻りたい場合もあるかもしれません。あるいは、処理を元に戻したいものの、これだけはやっておきたいという処理が残っている場合もあるかもしれません。

ANSI Common Lispではあらゆる言語の中でも最も柔軟性と高機能性を両立したと言えるような「コンディションシステム」が標準仕様として定められており、特殊な制御を要する代表例である「例外」はコンディションシステムを用いることが推奨されます。returnreturn-formなどは一般的にも利用されますが、blockcatchthrowなどを濫用するとプログラムは極めて読みにくくなってしまうため、より高水準なコンディションシステムなどの利用を検討してください。

名前付きブロックと脱出: block, return-from, return

blockスペシャルオペレータは名前付きのブロック構造を定義します。そのブロックの中からはreturn-fromスペシャルオペレータで脱出できます。特にブロックの名前がnilの時はreturnマクロで名前の指定をせずに脱出できます。

ここでは、リストの各要素の自然対数を取って返す場合を考えてみます。ANSI Common Lispは複素数に対応しているので、負数の log もエラーにはならず、複素数として返されます。しかし、今回は負数の対数は取りたくなく、負数が混じっていたらその負数自体を返して処理を終えたいとします。関数ではなくトップレベルでの評価式として記述すると以下のようになります。
(block log
  (mapcar #'(lambda (x)
              (if (minusp x)
                  (return-from log x)
                  (log x)))
          '(1 2 -3 4 5)))
; => -3
blockreturn-from/returnはセットで用います。しかし、実際にはreturn-fromreturnが単独で用いられる場合の方が多いです。それは、以下の場合にblockが暗黙的に用いられているためです。
  1. defunによる関数の定義は、同名のblockを定義します。
  2. 次章で説明する繰り返しの構文は、nilというblockを定義します。
前者はreturn-fromにて脱出でき、後者はreturnでも脱出できます。実際、脱出が必要なのは関数と繰り返しが多いため、blockを自ら使うことは稀です。
defunによる関数の定義を使うとblockがなくても済むためあえてblockを使ってみましたが、普通に考えれば以下のような関数定義になるでしょう。
(defun map-log (list)
  (flet ((log* (x)
           (if (minusp x)
               (return-from map-log x)
               (log x))))
    (mapcar #'log* list)))
; => MAP-LOG*

(map-log '(1 2 -3 4 5))
; => -3

ジャンプ: tagbody, go

Common Lispは非常に高水準で動的な言語ですが、歴史を見れば Lisp Machine のようにハードウェア用にも使われてきたため、現代の言語ではほとんど用いられていないような「ジャンプ」用のオペレータも備わっています。tagbodyスペシャルオペレータは複数の目印を立てることができ、goスペシャルオペレータはその目印に制御を移す(ジャンプする)ことができます。

このような制御を普段目にすることはないでしょうが、実際はよく使う機能の裏に隠されています。それが「ループ」です。非常に原始的なループをtagbodygoで記述すると、以下のようになります。
(defun loop-test (end)
  (let (i sum list)
    (tagbody
       (setq i 0)
       (setq sum 0)
       (setq list '())
     loop-area
       (when (= end i)
         (go end-area))
       (incf i)
       (incf sum i)
       (setq list (cons i list))
       (go loop-area)
     end-area
       (setq list (reverse list)))
    (values list sum)))
; => LOOP-TEST

(loop-test 10)
; => (1 2 3 4 5 6 7 8 9 10)
;    55
tagbodyはその名の通り tagbody を交互に記述します。そして、tagbodyの内部ではgoにより制御を移動させることができます。

ANSI Common Lispには非常に高機能なloopマクロがあるので、このような方法でループを実装してはいけませんが、tagbodyはループやコンディションシステムなどの基礎になっていますので、仕組みだけは知っておいても損はないと思います。

ダイナミックな制御: catch, throw

blockreturn-fromによる制御はとてもシンプルで、すでに述べた通り関数やループなどでの制御に用いられます。また、tagbodygoの組み合わせも柔軟で原始的な制御が可能なので、ループ構文の基礎として用いられています。

しかし、ANSI Common Lispではもう一歩ダイナミックな制御オペレータが定められています。それはcatchthrowです。

「ダイナミック」というのは、catchthrowがダイナミックスコープを持っているという意味です。つまり、制御の動作が文脈上決まる「レキシカルスコープ」ではなく、実行時に決まるということです。これは、複雑な制御が必要になる対話式のプログラムで力を発揮します。

対話式のプログラムで各動作を個別の関数に落とし込むと一つ一つの関数が小さな役割を持つため明確に記述できますが、その分だけ動作の制御が複雑になってしまいます。関数の原則は関数の最後の評価式で呼出し元に戻るというパターンであり、blockreturn-fromによる制御も呼出し元に戻ることに変わりありませんが、もっと動的な制御を可能にするためにはレキシカルな環境に左右されず、実行時の動作の履歴で制御を移動できる方が便利です。

ここでは、簡単な対話式プログラムを作ってみます。お掃除ロボット「ルンバ」を作っている iRobot 社はCommon Lispをメインの言語として使っているということは有名ですが、ルンバのように2次元(平面)を移動するロボットをイメージし、東西南北の移動コマンドを受け付けると共に、終了コマンドを受け取ると過去のコマンドを全て表示します。これらのコマンド以外が入力された場合は、再入力をするか終了するかを選ぶようにします。
仕様をまとめると以下の通りです。
  1. 東西南北の移動コマンドを受け付ける。
  2. 終了コマンドで終了する。その際は、過去のコマンド履歴を表示する。
  3. それ以外のコマンドの場合は、回復か終了を選ばせる。
以下がそのサンプルコードです。
;;;; direction.lisp
(defparameter *commands* '())
(defparameter *first-flag* t)

(defun init-game ()
  (psetq *commands* '()
         *first-flag* t))

;; 入力されたコマンドで分岐する部分
;; throw に注目
;; 投げる「タグ」を分けている
(defun move-to (v)
  (case v
    ((N S E W) (setq *commands* (cons v *commands*)) t)
    ((Q) (throw 'quit-game 'q))
    (t (throw 'unknown v))))

(defun get-command ()
  (when *first-flag*
    (format t "Select the direction:~%")
    (format t "  [N] = North, [S] = South, [E] = East, [W] = West~%")
    (format t "  [Q] = Quit.~%")
    (setq *first-flag* nil))
  (format t "(Current commands = [~{~a~^,~}])~%" (reverse *commands*))
  (format t "Enter your command: ")
  (read))

;; ゲームのメインループ
;; 'unknown タグが throw されると、ここで受け取る
(defun game-loop ()
  (let ((cmd (catch 'unknown
               (loop (move-to (get-command))))))
    (format t "[R] = Restart from when you entered [~a]~%" cmd)
    (format t "[Q] = Quit this game.~%")
    (case (read)
      ((R) (game-loop))
      ((Q) (throw 'quit-game 'q))
      (t (throw 'quit-game 'q)))))

;; ゲームを開始する関数
;; 'quit-game タグが throw されると、ここで受け取る
(defun start ()
  (init-game)
  (catch 'quit-game
    (game-loop))
  (format t "The End...~%")
  (format t "  Commands = [~{~a~^,~}]~%" (reverse *commands*))
  (format t "Bye!!~%")
  (values))

あまり綺麗なプログラムではないですが、catchthrowの部分に注目してください。blockreturn-fromはレキシカルな環境を持つので、必ずblockの中にreturn-fromがなければなりませんが、catchthrowはダイナミック環境を持つため、2つの配置はコンパイル時には決まりません。そのため、別々の関数にcatchthrowを配置し、catchをおいた関数からthrowを呼べば二つは動的に関連づけられるので、制御のコントロールが可能になります。
throwreturn-fromと同様に値を返すことができます。ここでは'unknownタグの時は返り値を使っていますが、'quit-gameタグの時は使っていません。返り値を使わなければ、単に脱出として利用できます。

なおloopというオペレータを使用していますが、これは simple loop と呼ばれる単純なマクロで、無限ループを作ります。高機能なloopと同じオペレータですが、ここでは単に「脱出しないと無限ループになるオペレータ」です。

以下がプログラムの実行例です。
(load "direction")
; => T

(start)
; Select the direction:
;  [N] = North, [S] = South, [E] = East, [W] = West
;  [Q] = Quit.
; (Current commands = [])
; Enter your command: s
; (Current commands = [S])
; Enter your command: e
; (Current commands = [S,E])
; Enter your command: w
; (Current commands = [S,E,W])
; Enter your command: n
; (Current commands = [S,E,W,N])
; Enter your command: d            ; ここで入力を間違えた 
; [R] = Restart from when you entered [D]
; [Q] = Quit this game.
; r                                ; 回復を選択した
; (Current commands = [S,E,W,N])
; Enter your command: e
; (Current commands = [S,E,W,N,E])
; Enter your command: q            ; 終了を選んだ
; The End...
;   Commands = [S,E,W,N,E]
; Bye!!

このような制御をデバッガと連結させてより柔軟にしたのが第9章で説明する「コンディションシステム」です。この例で見たように、例外的な処理は「通知」「捕捉」「回復」で構成されますが、コンディションシステムではより明確に記述できます。

必ず実行する処理: unwind-protect

throwcatchはとても柔軟な制御を可能にしますが、catchはどのような場所からthrowが投げられるか分からないため、「後処理」をするのには向いていません。「後処理」とは、throwによって制御をだっする場合でも行うべき処理などであり、例えばファイルオープンにおけるクローズに動作や、データベース接続の切断作業などです。これらは、途中で動作を離れても必ず処理する必要があります。

前節の例では、終了時にコマンド履歴を表示する機能は、必ず実行する処理に該当します。そこで、あえてunwind-protectを使って記述すると以下のようになります。ただ、違いがわかるように、一箇所だけunwind-protectを行わない終了を入れています。
;; unwind-protect で囲んだ throw
(defun move-to (v)
  (case v
    ((N S E W) (setq *commands* (cons v *commands*)) t)
    ((Q) (unwind-protect (throw 'quit-game 'q)
           (end-print)))
    (t (throw 'unknown v))))

;; unwind-protect で囲んだ throw と囲んでいない throw がある
(defun game-loop ()
  (let ((cmd (catch 'unknown
               (loop (move-to (get-command))))))
    (format t "[R] = Restart from when you entered [~a]~%" cmd)
    (format t "[Q] = Quit this game.~%")
    (case (read)
      ((R) (game-loop))
      ((Q) (unwind-protect (throw 'quit-game 'q)
             (end-print)))
      (t (throw 'quit-game 'q)))))

;; 必ず実行する部分を関数にした
;; unwind-protect で実行する
(defun end-print ()
  (format t "The End...~%")
  (format t "  Commands = [~{~a~^,~}]~%" (reverse *commands*))
  (format t "Bye!!~%"))

;; メインの関数は短くなった
(defun start ()
  (init-game)
  (catch 'quit-game
    (game-loop))
  (values))

実行例は以下の通りです。unwind-protectのない終了とある終了を試すため、2回実行しています。最初の実行例がこちらです。
(start)
; Select the direction:
;   [N] = North, [S] = South, [E] = East, [W] = West
;   [Q] = Quit.
; (Current commands = [])
; Enter your command: e
; (Current commands = [E])
; Enter your command: s
; (Current commands = [E,S])
; Enter your command: d             ; 入力を間違えた
; [R] = Restart from when you entered [D]
; [Q] = Quit this game.
; f                                 ; また間違えたので表示はしない

次の実行例がこちらです。unwind-protectで囲った表示処理が行われています。
(start)
; Select the direction:
;   [N] = North, [S] = South, [E] = East, [W] = West
;   [Q] = Quit.
; (Current commands = [])
; Enter your command: e
; (Current commands = [E])
; Enter your command: s
; (Current commands = [E,S])
; Enter your command: d             ; 入力を間違えた
; [R] = Restart from when you entered [D]
; [Q] = Quit this game.
; q                                 ; 終了コマンドなので表示する
; The End...
 ;  Commands = [E,S]
; Bye!!
unwind-protectスペシャルオペレータは第1引数である protected-form を評価しますが、その評価中に制御(強制的な動作の移動)が発生した場合も第2引数以降の cleanup-forms を評価してから移動します。protected-form は1つしか引数を取れないので、複数の式を評価する場合はprognスペシャルオペレータを使ってください。

0 件のコメント :

コメントを投稿