Common Lisp Beginner's Guide

Chapter 3. 評価とコンパイル

概要

ANSI Common Lispの第3章のうち、評価モデル, ラムダクロージャ, 多値, ラムダリスト, コンパイル, 宣言, エラーチェックについて説明します。

  1. 評価モデル
    1. シンボル
    2. リスト
    3. 自己評価値
  2. ラムダとクロージャ
    1. 無名関数lambda
    2. レキシカルクロージャ
    3. スペシャル変数
  3. 多値
  4. ラムダリスト
  5. コンパイル
  6. 宣言
  7. エラーチェック

評価モデル

Common Lispの評価モデルはとても単純です。評価の対象となる「フォーム( form )」は3種類しかありません。

  1. シンボル
  2. リスト (1)
  3. 自己評価値

シンボル

フォームとしてシンボルが現れた場合、それは「変数」または「シンボルマクロ」として評価されます。

Common Lispにおいて変数は、値と関数の両方を保持することができます。そしてLispの世界では、変数に値を「代入する」とは言わず、「束縛( bound )する」と呼びます。

変数を束縛するには以下のようなオペレータを使います(代表的な例)。

Macro: defvar
グローバル変数を定義します。一度定義すると再定義することはできません。
Macro: defparameter
グローバル変数を定義します。再度定義した場合は、値が更新されます。
Special Operator: let
ローカル変数を定義します。手前の変数の定義は、次の定義では参照できません。
Special Operator: let*
ローカル変数を定義します。手前の変数の定義が、次の定義で参照できます。
Special Form: setq
一度定義されている変数の値を変更します。

以下がサンプルです。Common Lisp処理系を立ち上げて随時入力してみてください。

;; グローバル変数の定義
(defvar *var* 1)
; => *VAR*
(defparameter *para* 2)
; => *PARA*

;; ローカル変数の定義
(let ((a 1)
      (b 2))  ; aを使うことはできない (parallel)
  (list a b))
; => (1 2)

(let* ((a 1)
       (b (+ a 1))) ; aを使うことができる (serial)
  (list a b))
; => (1 2)

;; 再定義
(defvar *var* 3)
; => *VAR*
*var*
; => 1     ; defvarは再定義できない

(defparameter *para* 4)
: => *para*
*para*
; => 4     ; defparameterは再定義できる

;; 値の変更(defvarもdefparameterもできる)
(setq *var* 5)
; => 5
(setq *para* 6)
; => 6

リスト

評価の対象となる式がリストの場合は以下の3種類あります(簡単のため、「ラムダフォーム」はここでは省略します)。

  1. スペシャルフォーム
  2. マクロ
  3. 関数

これらは全て同じ形式で適用されますが、動作や意味が少し異なります。

適用される形式はこれまで見てきたように(operator arguments*)という形式です。リストの第1要素がオペレータで、それ以外は引数になります。

動作の違いは以下の通りです。

スペシャルフォーム( Special Form )
スペシャルフォームはそれ以上変形できない根源的なオペレータです。例えば、条件分岐を行うifや引用を示すquoteなどは他のオペレータで表現することができないため、スペシャルオペレータとして定義されています。
マクロ( Macro Form )
他のスペシャルフォームや関数に変形するための形式です。「このソースコードはこう書きたい」という思いを実現します。例えば、条件分岐が複数に渡る場合、ifというスペシャルフォームをいくつも重ねるのは美しくないため、condというマクロを定義して、ソースコードの変形によりifの機能を実現します。マクロはdefmacromacroletなどで定義します。
関数( Function Form )
関数は第1級( First Class )のデータオブジェクトです。変数に束縛された値のように引数に渡したり、関数の返り値として関数を返すこともできます。マクロと異なり、引数は全て評価されてから関数に渡ります(先行評価)。defunfletlabelsなどで定義します。

自己評価値

自己評価値( Self-Evaluating Objects )とは、そのままの値として評価されるものです。例えば数や文字列、ベクトルなど今までにもいくつか使ってきた「値」です。

"Hello"
; => "Hello"  ; 文字列

3.14
; => 3.14     ; 数

1/8
; => 1/8      ; 分数

#(1 2 3)
; => #(1 2 3) ; ベクトル(1次元配列)

#2a((1 2) (3 4))
; => #2A((1 2)(3 4)) ; (2次元)配列

#p"~/common-lisp/"
; => #P"/Users/satoshi/common-lisp/" ; パスネーム

ラムダとクロージャ

本節ではLispの最も重要な特徴とも言える「ラムダ」と、その応用である「レキシカルクロージャ」について説明します。

無名関数lambda

「関数」には一般に名前がつけられます。なぜなら名前がないと関数を呼ぶ(適用する)ことができないからです。例えば、数学で f(x) = 3x + 1 という関数を適用する場合は f(2) のように記述しますが、このとき f という関数名がなければ適用することができません。

しかし、逆に言えば関数の適用について一定の規則を定めれば、名前がなくても適用することができます。Common Lispにおける規則はこれまで使ってきた「前置記法」です。つまり、リスト(S式)の先頭に関数を置けば、残りの要素が引数として適用されます(2)。

((lambda (x) (expt x 3)) 3)
; => 27

上記の例では、3乗する無名関数を作成し、リストの先頭に配置しています。そして、引数として3を渡しています。

このように、Common Lispでは必ずしも関数に名前をつける必要はなく、lambdaを用いた無名関数は頻繁に用いられます。

補足: What is lambda ?

ところでlambdaは何者なのでしょうか。

すぐに分かるのは、「関数」ではないということです。上の例ではxという未定義のシンボルが使われていますが、lambdaが関数なら引数のxが先に評価されるため、未定義エラーとなるはずです。lambdaは無名関数を作り出すのですが、関数ではないのです。

また、ifのようなスペシャルオペレータでもありません。ifを用いたS式はデータとはならず、if文の評価の結果がデータとなりますが、lambdaの文はそれ自体でデータとなります。例えば以下の例を見てください。

(defvar expt3 (lambda (x) (expt x 3)))
; => EXPT3
(mapcar expt3 '(1 2 3 4 5))
; => (1 8 27 64 125)

lambdaを含む文(または式)は変数expt3に束縛することができます。2行目のmapcarは第2引数以降のリストの各要素を1つずつ取り出して第1引数の関数に適用します。関数自体を引数として受け取るので、「高階関数」と呼ばれます。このようなことはスペシャルオペレータではできません。

結論を言うと、lambdaは究極的にはただのシンボルです。それが無名関数を表す式であることを示すマークです。lambdaは「ラムダ計算」と言う計算モデルのキーワードですが、そこで使われている λ というマークの代わりです。このマークは xi のような変数とは違い、無名関数であることを示す役割を担っています。

lambdaはただのシンボルなので、この式から実体としての関数を作らなければなりません。その役割を担うのがfunctionスペシャルオペレータです。

(function (lambda (x) (expt x 3)))
; => #<Anonymous Function ...>

functionはシンボルlambdaのある式から無名関数(Anonymous Function)を作り出します。よって、mapcarの文は以下のように書くこともできます。

(mapcar (function (lambda (x) (expt x 3))) '(1 2 3 4 5))
; => (1 8 27 64 125)

lambdaの前にfunctionを書いていくのは面倒なので、実はlambdaにはマクロも登録されており、functionがなくlambdaが使われた場合、マクロとしてのlambdaが使われ、(function (lambda ...))の形式に展開されます。そのため、functionは記述する必要はありません。

ただし、実はlambdaの前にfunctionを付加するスタイルも使われています。それは、無名ではない関数との整合性からです。例えば、Common Lispには1+と言う関数があり、引数に1を加算するだけのものですが、これを高階関数に適用する場合、functionを付けなければなりません。

(mapcar 1+ '(1 2 3 4 5))
; => Error: Unbound variable: 1+

何も付けずにいるとエラーになります。関数1+はシンボルlambdaと異なりfunctionを追加するようなマクロがないので当然です。

(mapcar (function 1+) '(1 2 3 4 5))
; => (2 3 4 5 6)

functionをつけると1+というシンボルに束縛された関数が呼び出されてきて、mapcarに引数として渡されます。このように、functionには2つの役割があります。

functionスペシャルオペレータは高階関数や無名関数で多用されるため、#'というディスパッチリーダーマクロが登録されています。よって、実際によく目にするのは以下のように#'を使用したものです。

(mapcar #'1+ '(1 2 3 4 5))
; => (2 3 4 5 6)

これと同じようにするため#'lambdaにも付ける場合があるのです。

(mapcar #'(lambda (x) (expt x 3)) '(1 2 3 4 5))
; => (1 8 27 64 125)

ANSI Common Lispではこの扱いが標準として定められているためlambda#'を付けるかどうかは好みの問題ですが、他のソースコードを修正・保守する場合は既存のコーディングスタイルに合わせるべきです。

レキシカルクロージャ

Common Lispでのプログラミングでlambdaはいたるところに登場しますが、それは単に無名関数を作れるからという理由ではありません。無名関数は「名前が無い」だけで、名前のある関数で代用できるなら無名関数自体が不要になります。

lambdaがより強力なプログラミングをサポートするのは、それ(functionで生成される関数の実体)が本物のデータであることを利用する場合です。データというのは計算における「状態」を意味します。例えば、以下で数を数える「カウンター」を作る例を示します。

(defun make-counter ()
  (let ((n 0))
    #'(lambda () (incf n))))
; => MAKE-COUNTER

(defvar counter (make-counter))
; => COUNTER

(funcall counter)
; => 1
(funcall counter)
; => 2
(funcall counter)
; => 3

make-counter関数はletでローカルな変数nを束縛した後、lambdaを返します。関数の返り値として無名関数を返しているのです。この時最も重要なのは、ローカルな変数nの扱いです。Common Lispではletで束縛された変数がlambdaの上にある時、lambdanの状態を内包して無名関数として生成されます。つまり、nはローカルな領域を抜けても関数実体の中に存在し続けます。ここではこれをカウンタの状態保存変数として利用しています。incfは1を加算する関数で、引数の状態を上書きする点が1+関数とは異なります。

(make-counter)で作られた無名関数#'(lambda () (incf n))は変数counterに束縛されます。変数に束縛された関数はデータとして扱うことができますが、funcall関数によってデータではなく動作可能な関数として適用されます。そのため、(incf n)が評価されて1ずつ増えていきます。

このように、状態を包み込んだ関数の実体を「レキシカルクロージャ」(または単に「クロージャ」)と呼びます。また、Common Lispではletで束縛されたローカルな変数もクロージャに包まれると範囲を超えて参照可能となるため、「グローバル」「ローカル」という変数の分け方を一般には用いず、「ダイナミック」「レキシカル」という区別をします。

スペシャル変数

ダイナミックな変数はスペシャル変数とも呼ばれます。これまでに何度か使ってきたdefvarはスペシャル変数を定義するオペレータで、レキシカル変数を束縛するletとは異なる動作をします。

以下の例を見てください。

(defvar n2 0)
; => N2

(defun make-counter1 ()
  (let ((n1 0))
    #'(lambda () (incf n1))))
; => MAKE-COUNTER1

(defun make-counter2 ()
  (let ((n2 0))
    #'(lambda () (incf n2))))
; => MAKE-COUNTER2

(defvar counter1 (make-counter1))
; => COUNTER1
(defvar counter2 (make-counter2))
; => COUNTER2

(let ((n1 10)
      (n2 10))
  (print (funcall counter1))
  (print (funcall counter2)))
; 1
; 11

n2
; => 0

関数定義上は全く同じに見えるmake-counter1make-counter2を定義しています。ここで、make-counter2に使用しているn2という変数は、defvarを使って定義されているため、スペシャル変数として認識されます。スペシャル変数の何がスペシャルかというと、変数の参照が定義時ではなく実行時に決まるという点が特別です。

下の方のletではn1n2も共に10が束縛されていますが、counter1はその影響を受けません。これは、同じn1でもカウンタ(クロージャ)の中のn1とは別のものであることを示しています。

他方、counter2はスペシャル変数n210に束縛されているため、そこから1が加算されて11と表示されます。これは、クロージャの内部(定義時の状態)と外部(実行時の環境)が区別されず、参照されていることを示します。変数の参照範囲が定義時に決まることを「レキシカルスコープ」と呼び、実行時に決まることを「ダイナミックスコープ」と呼びます。defvarは後者の変数を定義します。

なお、一番最後にn2の値を参照していますが、返り値は0です。スペシャル変数がletの中で別の値に束縛された時、そのletを抜けると元の値に戻ります。スペシャル変数はクロージャを作るのには向いていませんが、一時的な値の変更を伴う「パラメータ」としては便利です。

補足: マクロを使ったバージョン

(注意)この節は Scheme を学んだことのある中級者向けです。初心者は読み飛ばしてください。

Schemeを学んだことのある人はfuncallによる適用に違和感を感じるかもしれません。関数と変数が完全に区別されない Scheme ではfuncallが不要であり、一貫性があるように見えます。

もちろん、いわゆるLisp-1Lisp-2という違いはあるのですが、Common Lispの場合はマクロを使って Scheme 風に書くことも可能です。カウンタを作って変数に定義付けるのではなく、無作為な名前の変数にカウンタの実体(クロージャ)を定義付け、カウンタはそのクロージャをfuncallする関数として定義すれば良いのです。変数と関数を同時に定義するようなマクロは以下のようになります。

(defmacro make-counter* (counter-name)
  (let ((sym (gensym)))
    `(progn
       (defvar ,sym
         (let ((n 0))
           #'(lambda () (incf n))))
       (defun ,counter-name ()
         (funcall ,sym)))))

(make-counter* counter)
; => COUNTER

(counter)
; => 1
(counter)
; => 2
(counter)
; => 3

無駄に複雑になるのであまりオススメはしませんが、実はCommon Lispの構造体(Structure)などもこのようなマクロを駆使して作られています。「funcallを使うのが気持ち悪い」という理由だけでSchemeを使用することはオススメできません。

参考までに、マクロを展開したらどのようになるかも掲載しておきます。ANSI Common Lispはpprint(Pretty Printer)やmacroexpand-1(1回だけマクロ展開する)のような関数も標準仕様として定められています。

(pprint (macroexpand-1 '(make-counter* counter)))
; (PROGN (DEFVAR #:G549 (LET ((N 0)) #'(LAMBDA NIL (INCF N))))
;        (DEFUN COUNTER () (FUNCALL #:G549)))

多値(multiple-values)

Common Lispの関数の特徴として前節までに「レキシカルクロージャ」を説明しましたが、もう一つ他の言語にはない珍しい機能があります。それは「多値(multiple values)」です。

C言語をはじめとして多くの言語は関数の返り値は1つだけです。しかし、数学の世界を見れば、2次方程式の解が2つ存在するように、複数の値をその関数の返り値として返したい場合があります。もちろん、リストなど複数の値をまとめる方法で返せばいいのですが、その場合は返り値を受け取った側で分解する処理が必要です。つまり、返す関数ではリストを構築し、受け取る関数(呼び出した関数)では返り値を分解してまた新しい変数に束縛しなければなりません。

Common Lispでサポートされる多値はこれらの処理をネイティブに実現するものです。代表的な多値の具体例として2つ示します。1つ目は割り算の整数解と余りです。

(let ((x 10)
      (y 3))
  (multiple-value-bind (q r)
      (truncate x y)
    (format t "~a / ~a = ~a ... ~a~%" x y q r)))
; 10 / 3 = 3 ... 1

除算を行うtruncate関数は、整数解と剰余を多値として返します。戻ってきた多値はmultiple-value-bindマクロによって即座にqrという変数に束縛されます。formatはここでは説明しませんが、それぞれ変数の値を表示しています。

2つ目の例は日時です。現在の日時を取得するget-decoded-time関数は多値を返します。

(defun weekday (w)
  (case w
    (0 "Mon.")
    (1 "Tue.")
    (2 "Wed.")
    (3 "Thu.")
    (4 "Fri.")
    (5 "Sat.")
    (6 "Sun.")))

(multiple-value-bind (s m h dd mm yyyy w)
    (get-decoded-time)
  (format t "~a/~a/~a ~a:~a:~a (~a)~%"
          yyyy mm dd h m s (weekday w)))
; 2017/10/6 0:49:23 (Fri.)

実はget-decoded-time関数はここで受け取っている値以外にもサマータイムかどうかの真偽値とタイムゾーンも合わせて多値で返してくるのですが、使わないのでbindしていません。このように、多値は不要であれば全てを受け取る必要はありません。

では逆に多値を返すにはどうすれば良いのでしょうか。こちらも単純で、valuesで多値を構築することができます。

(values 2017 10 6)
; => 2017
;    10
;    6

多値は一般のCommon Lisp処理系の場合、改行されて表示されるのですぐにわかります。EmacsでSLIMEを使用している場合はセミコロンで区切られて表示されます(ミニバッファのとき)。関数の末尾(tail position)にこのvaluesをおけば多値が関数の返り値として使われます。

参考: 構造化代入(destructuring-bind)

多値は便利ですが、関数が多値ではなくリストで返すように設計されていた場合、multiple-value-bindは諦めて自分でリストの分解をしなければならないのでしょうか。

ANSI Common Lispにはそのような時に備えて、値の分解と束縛を同時に行うマクロdestructuring-bindが用意されています。

(destructuring-bind (yyyy mm dd h m s w)
    '(2017 10 6 0 49 23 4)
  (format t "~a/~a/~a ~a:~a:~a (~a)~%"
          yyyy mm dd h m s (weekday w)))
; 2017/10/6 0:49:23 (Fri.)

destructuring-bindはより複雑な分解にも対応しています。わざと複雑な例として、九九を表示してみました。

(destructuring-bind (row col (&rest rest))
    (let ((r 9)
          (c 9))
      (list r
            c
            (loop for x from 1 to r
               collect (loop for y from 1 to c collect (* x y)))))
  (format t "===== ~a × ~a =====~%" row col)
  (dolist (v rest)
    (format t "~{~2d~t~^~}~%" v)))

出力結果は以下のようになります。説明していない項目がたくさん出てきているので、分からなければ読み飛ばしてください。

; ===== 9 × 9 =====
; 1  2  3  4  5  6  7  8  9 
; 2  4  6  8 10 12 14 16 18 
; 3  6  9 12 15 18 21 24 27 
; 4  8 12 16 20 24 28 32 36 
; 5 10 15 20 25 30 35 40 45 
; 6 12 18 24 30 36 42 48 54 
; 7 14 21 28 35 42 49 56 63 
; 8 16 24 32 40 48 56 64 72 
; 9 18 27 36 45 54 63 72 81 

ラムダリスト(Lambda List)

Common Lispにおいてdefunlambdaで構築される「関数」は非常に重要な役割を担うため、その引数の扱いも他の言語に比べて充実しています。引数にはいくつかの異なる種類が存在しますが、主に使用されるものは以下の4つです。

必須引数
必ず指定しなければならない引数です。何も指定しなければ必須引数となります。
任意引数(&optional)
指定してもしなくてもよい引数です。指定されない場合のためにデフォルト値を付けることができます。
可変長引数(&rest)
引数の個数を指定しない引数です。全てまとめられて1つのリストとして処理されます。
キーワード引数(&key)
キーワード形式(:key value)で指定することのできる引数です。任意引数と同様にデフォルト値を付けることができます。

これらは組み合わせることができますが、後ろの3つ(任意・可変長・キーワード)の間では組み合わせて使うことは推奨されません。つまり、以下のようなパターンで使用してください。

  1. 全ての引数を必須にできる場合は、必須引数として定義してください。最もシンプルです。
  2. 柔軟性を確保する目的で必須にしたくない場合は、任意引数キーワード引数かのどちらかを使用するようにしてください。
  3. 可変長引数を用いる場合は必須引数としか組み合わせるべきではありません。

以下では「文字列を区切り文字で連結する」関数を定義して、使い方を示します。

まず、全ての引数を必須とする場合は、以下の通りシンプルに定義できます。これは2つの文字列を引数に取り、カンマ(",")で連結して返します。

(defun join1 (string1 string2)
  (concatenate 'string string1 "," string2))
; => JOIN1

次に、区切り文字のカンマを指定できるように変更する場合、任意引数かキーワード引数のどちらかを使います。

; 任意引数の場合
(defun join2 (string1 string2 &optional (sep ","))
  (concatenate 'string string1 sep string2))
; => JOIN2
(join2 "abc" "123" " ")
; => "abc 123"
  
; キーワード引数の場合
(defun join3 (string1 string2 &key (sep ","))
  (concatenate 'string string1 sep string2))
; => JOIN3
(join3 "abc" "123" :sep " ")
; => "abc 123"

任意引数の場合、引数の並びが重要になるため、関数の利用者は引数の並びを覚えなければなりません。他方、キーワード引数は並びに関係なく使えるため、キーワードの分だけ引数は多くなりますがより分かりやすく記述することができます。

そして最後に、可変長引数を扱ってみます。複数の文字列を全て連結するような場合です。この場合、可変長引数と任意/キーワード引数の併用は推奨されませんから、区切り文字は任意/キーワード引数ではなく必須引数して定義します。

(defun join4 (sep &rest strings)
  (unless sep (setq sep ","))
  (flet ((join% (v1 v2)
           (concatenate 'string v1 sep v2)))
    (reduce #'join% strings)))
; => JOIN4
(join4 nil "abc" "def" "ghi")
; => "abc,def,ghi"

このように可変長引数を使うために必須引数にするような場合はnilを指定したらデフォルト値で動くように設計します。unlessマクロは第1引数がnilの時に本体部分を評価します。

ここで使われているfletオペレータはletの関数版で、関数の内部でローカルな関数を定義できます。また、reduce関数は「縮約関数」や「畳み込み関数」と呼ばれている高階関数で、引数を2つ取って1つの値として返し、その返した値と新しい引数の2引数を取ってまた1つの値として返す、という動作を繰り返すものです。文字列の連結は本来的に2つを1つにしていく動作なので、reduceが使えます。

なお、ここでは可変長引数を使うために以上のように定義しましたが、実際のプログラミングでは以下の2通りの方法も検討してください。

まず、Perl など他の言語のjoinは第2引数として文字列の配列を指定することが大半で、可変長引数としては設計されていません。よって、文字列結合関数は無理に可変長引数を使わなくても、必須引数2つのシンプルな関数として定義するのが最も分かりやすいでしょう。

(defun join (sep list)
  (unless sep (setq sep ","))
  (flet ((join% (v1 v2)
           (concatenate 'string v1 sep v2)))
    (reduce #'join% list)))
; => JOIN

(join nil '("abc" "def" "ghi" "123"))
; => "abc,def,ghi,123"

また、ANSI Common Lispで標準化されているformatは大変強力な出力関数で、それ自体に反復を行う機能が付いています。そのため、そもそも関数を定義しなくてもいいとも考えられます。

(format nil "~{~a~^,~}" '("abc" "def" "ghi" "123"))
; => "abc,def,ghi,123"

formatを使う場合は、リストでも可変長でも臨機応変に対処できます(3)。

(format nil "~@{~a~^,~}" "abc" "def" "ghi" "123")
; => "abc,def,ghi,123"

ラムダリストは実は関数以外に様々な種類があります。以上で説明したもの以外によく使うのはマクロの定義(defmacro)で用いる&bodyキーワードですが、これは&restと同じ可変長引数を扱うもので、唯一想定されるインデントが異なります。&restの引数は全て位置が整えられて表示されますが、&bodyの場合は2つ目の引数のインデントが変わります。これは慣例に従ったもので、SLIMEを使用していると自動的に整形してくれます。

他のラムダリストについては出現した時に適宜説明します。

補足: nilsupllied-p

かなり応用に近い補足なので、読み飛ばしても問題ありませんが、任意引数やキーワード引数のデフォルト値がnilである場合について少しだけ説明します。

全く何の機能もありませんが、任意引数が指定されているかどうかだけを判定する関数を以下のように定義したとします。

(defun is-argument (&optional (x nil))
  (if x t nil))
; => IS-ARGUMENT

この関数は基本的には目的を果たしますが、不幸にも引数がnilだった場合には引数が渡されてきているにも関わらずnilを返してしまいます。

(is-argument 'null)
; => T
(is-argument 'nil)
; => NIL

任意引数やキーワード引数の欠点として、デフォルト値を指定された場合に、指定されたのか指定されていないのかが判別できないということが挙げられます。しかし、Common Lispではそのような場合に備えて、引数が渡されてきたかどうかを調べる機能もラムダリストに組み込まれています。それがsupplied-pです。

(defun is-argument (&optional (x nil x-supplied-p))
  (values x x-supplied-p))
; => IS-ARGUMENT

引数名に-supplied-pを付けると、引数が指定されたか否かの真偽値が束縛されます。多値を使って渡されてきた引数自体と引数が渡されてきたか否かを合わせて返せば、関数適用の状況を正確に把握することができます。

(is-argument 'nil)
; => NIL
;    T

(is-argument)
; => NIL
;    NIL

使う場面はそう多くないかもしれませんが、nilはANSI Common Lispで様々な意味を持っているため、このような処理が組み込まれていることを知っておくと何かに役立つかもしれません。

コンパイル(compile)

ANSI Common Lispには言語の仕様だけでなく、コンパイルについても定められています。コンパイルとはある言語のソースコードを別の言語や機械語に変換する作業です。Common Lispの処理系によっては機械語に直接変換するものもあれば、C言語やJavaScriptに変換するものもあり、バイトコードに変換するものもありますが、ANSI Common Lispでコンパイルが定められているので、必ず以下の2つの関数が用意されているはずです。

compile関数
関数単位でコンパイルを行います。
compile-file関数
ファイルに含まれる関数全てに対してコンパイルを行います。

他の言語と異なる点は、関数単位でコンパイルを行うことができる点です。コンパイルした関数は高速に動作するため、コンパイルしていない状態のものを自動で置き換えます。

Steel Bank Common LispやClozure Common Lispは純粋なインタプリタを持たず、コンパイルして実行するのが原則です。そのため、GNU CLISPで以下のように試してみてください。

(defun test (x) (print x))
; => TEST
(test "Hello")
; "Heloo"
; => "Hello"
#'test
; => #<FUNCTION TEST (X) 
;      (DECLARE (SYSTEM::IN-DEFUN TEST)) 
;      (BLOCK TEST (PRINT X))>
; コンパイルされていない生の関数が表示される

; コンパイルを行う
(compile 'test)
; => TEST;
;    NIL;
;    NIL
; 動作は同じ
(test "World")
; "World"
; => "World"
; ただし、すでにコンパイルされている
#'test
; => #<COMPILED-FUNCTION TEST>

GNU CLISPの場合、インタプリタで入力した関数やloadでロードしてきた関数はまだコンパイルされていません。その方がエラーメッセージなどが分かりやすいのですが、インタプリタは動作が遅いため、動作確認が取れたらコンパイルするようにしてください。

なお、compile関数の返り値は多値です。コンパイルした関数名、警告の有無、失敗の有無が返されます。

compile-fileとマクロ

ファイル内の全ての関数をコンパイルしたい場合、(compile-file "filename.lisp")とすればコンパイルすることができ、.fasl.fas などのコンパイル済みファイルが作られます。次からloadすると自動的にこれらのファイルが使われます(4)。

しかし、マクロを含んでいるファイルをloadしているようなケースは注意が必要です。以下のような2つのファイルを用意してください。まず、マクロを含むファイル test.lisp はこちらです。

;;;; test.lisp

(defmacro nil! (x)
  `(setq ,x nil))

(defun test (x)
  (print x))

これは変数をnilにセットするマクロnil!の定義です。また、引数を表示する関数testも定義しています。そして、そのマクロを使うファイル test2.lisp はこちらです。

;;;; test2.lisp

(load "test")

(defparameter a 1)

(test a)
(nil! a)
(test a)

マクロ定義の test.lisp を読み込んで使用しています。

このファイル test2.lisp をそのままコンパイルすると、エラーも警告もなくそのまま通ります。(処理系によってはメッセージが表示されます。)

(compile-file "test2")
; 0 errors, 0 warnings
; => #P"/Users/satoshi/Desktop/test2.fas" ;
;    NIL ;
;    NIL

しかし、ロードするとエラーになります。

(load "test2")
; *** - FUNCALL: NIL! is a macro, not a function
; The following restarts are available:
; SKIP           :R1      skip 11 11 (NIL! A)-5
; RETRY          :R2      retry 11 11 (NIL! A)-5
; STOP           :R3      stop loading file test2.fas
; ABORT          :R4      Abort main loop

Common Lispは動的な言語なので、関数の参照は実行時のLisp イメージに依存します。Lispイメージは実行時にロードされている関数や変数などの集合です。

しかし、マクロは関数と異なり、コンパイラに対する命令です。そのため、実行時ではなくコンパイル時に定義が必要です。もちろん、マクロ定義を実行時にも追加で使う場合はコンパイル時と実行時に共に必要です。

よって、マクロの定義はコンパイル時に一度評価され、Common Lispが理解できる状態にしなければなりません。今回の test2.lisp の場合、(load "test")の部分はコンパイル時に評価されないため、(nil! a)というコードを変形(マクロ展開)することができません。しかし、Lispは「nil!は関数なのだろう」と解釈し、その定義が実行時には定義されている可能性もありますから、エラーや警告にはなりません。

このような問題を避ける方法は2つあります。

  1. マクロ定義をloadしてからcompile-fileする。
  2. eval-whenを使用する。

1の方法は単純です。compile-file関数を使ってコンパイルする前にloadでマクロ定義の test.lisp を先に読み込んでコンパイラにnil!マクロの存在を知らせるだけです。

2の方法は少し複雑ですが、より堅牢なシステムになります。すでに説明したように、実行時ではなくコンパイル時にマクロ定義を知りたい訳ですから、コンパイル時に(load "test")の部分を評価するようにすれば良いのです。(load "test")の部分を以下のように置き換えてみてください。

;;;; test2.lisp

(eval-when (:compile-toplevel :load-toplevel :execute)
  (load "test"))

eval-whenスペシャルオペレータは評価のタイミングを指定できます。多くの場合は3つ(:compile-toplevel, :load-toplevel, :execute)を全て指定します。こうするとcompile-fileを使用した場合に(load "test")が先行して読み込まれ、マクロ定義nil!を知ることができます。

ANSI Common Lispに定められたコンパイルはこのように使いますが、実際のプログラミングではASDFというデファクトスタンダード(事実上の標準)のシステム管理ツールが存在するため、あまり意識する必要はありませんが、知っておいて損はありません。

宣言

コンパイルは高速化に大きな効果を発揮しますが、適切な「宣言」を付けることでより高速化が期待できる場合があります。「宣言」はコンパイラに対するプログラマからの指示です。Common Lispには様々な宣言が用意されていますが、よく使うのは以下の4種類です。

type (declareで関数内に指定)
変数の型を指定します。
ignore (declareで関数内に指定)
変数が使われなくても警告を出しません。
inline (declaimで関数の手前に指定)
関数をインライン化(埋め込み)します。
optimize (declareで関数内に指定)
関数を最適化します。最適化の方法は以下の5つを組み合わせます。0(重要でない)から3(極めて重要)までの整数で指定しますが、省略すると3が指定されたことになります。

どこに、どのように指定するかも少し違いますのでカッコ内を参考にしてください。

また、関数の返り値の型を指定するtheスペシャルオペレータも最適化において利用されます。

参考: disassembleによる実証

どのように指定するかは実例を見るのが一番だと思いますので、type, optimize, theを使用した例を示します。

まず、非常に単純な関数fを定義してください。(このfはANSI Common Lispのdisassembleの項に掲載されているものです。)

(defun f (a) (1+ a))
; => F

そして、disassemble関数を使ってみてください。

(disassemble 'f)

出力結果は以下のようになりました。(ここではClozure CLでの結果を示します。)

;;; (defun f (a) (1+ a))
    (recover-fn-from-rip)                   ;     [7]
    (cmpl ($ 8) (% nargs))                  ;    [14]
    (jne L57)                               ;    [17]
    (pushq (% rbp))                         ;    [19]
    (movq (% rsp) (% rbp))                  ;    [20]
    (pushq (% arg_z))                       ;    [23]

;;; (1+ a)
    (leaveq)                                ;    [24]
    (testb ($ 7) (% arg_z.b))               ;    [25]
    (jne L39)                               ;    [29]
    (addq ($ 8) (% arg_z))                  ;    [31]
    (jo L32)                                ;    [35]
    (repz)
    (retq)                                  ;    [37]
L32
    (lisp-jump (@ .SPFIX-OVERFLOW))         ;    [39]
L39
    (movl ($ 8) (% arg_y.l))                ;    [46]
    (lisp-jump (@ .SPBUILTIN-PLUS))         ;    [51]

;;; #<no source text>
L57
    (uuo-error-wrong-number-of-args)        ;    [64]

次に、安全性ではなく高速性を優先させた宣言付きのコードを定義してください。

(defun f (a)
  (declare (type fixnum a)
           (optimize (speed 3) (safety 0)))
  (the fixnum (1+ a)))

同様にdisassembleしてみます。

(disassemble 'f)

こちらの出力結果は先ほどに比べて短くなりました。

;;; (defun f (a) 
;;;   (declare (type fixnum a) 
;;;            (optimize (speed 3) (safety 0))) 
;;;   (the fixnum (1+ a)))
    (recover-fn-from-rip)                   ;     [7]
    (pushq (% rbp))                         ;    [14]
    (movq (% rsp) (% rbp))                  ;    [15]
    (pushq (% arg_z))                       ;    [18]

;;; (1+ a)
    (addq ($ 8) (% arg_z))                  ;    [19]
    (leaveq)                                ;    [23]
    (retq)                                  ;    [24]

型の指定はリストやベクトルの場合、かなり複雑な「型指定子」を使うことになります。また、関数の返り値へのtheでの指定はソースコードの可読性を大幅に低下させます。そして、高速化を施した後の関数は次節で述べるようなエラーチェックが省略されるため、予定外の入力(引数)に対する動作が不安定になります。

エラーチェック

関数を適用する際にどのようなチェックを行うかは処理系依存の面が強いですが、以下のような項目はANSI Common Lisp(第3章5節 Error Checking in Function Calls)で定められているため、optimize宣言でsafetyを引き下げるかspeedを引き上げていない限り、安全に処理されます。「安全に」とはシステムが不意に停止してしまうのではなく、デバッグモードが立ち上がって正常に復帰できるような状態を指します。

Common Lispにおけるプログラムは関数の連鎖によって動作しますから、関数の引数の安全性を確保することによってプログラム全体の安全性を高めることができます。

なお、引数の型の正しさまで確保したい場合はdeclareによるtypeの指定ではなく、check-typeマクロを使います。これは型の指定が異なっているとエラーを通知するため、実際の処理を行ってエラーが発生するよりも前にデバッガに入ることができます。declaretypeの組み合わせはあくまでも高速化が念頭にあり、目的が異なることに注意してください。


  1. 正確には「コンス」です。リストはコンスの連鎖なのですが、コンス及びリストについては第14章で扱います。
  2. 評価モデルの4つ目の法則ですが、高階関数のような状況ではなく無名関数を直接的に適用するケースはあまり多くないと思いますので、評価モデルの節では省略しています。
  3. formatは別の機会に説明しますが、ここで使っている機能は主に以下の5つです。
    1. 第1引数にnilを指定すると出力ではなく文字列として値を返します。(tの場合は標準出力に出力します。)
    2. ~aは引数をうまく表示します。「うまく」というのは引数が文字列でも数でも「いい感じ」に表示してくれるということです。
    3. ~{は反復の開始を示します。また、~}は反復の終了を示します。
    4. ~^は反復終了時にここで終わることを示します。つまり、例のように~^,と書いてある場合、反復が終わる時には","が出力されずに手前で終了することを意味します。
    5. ~@{は引数を再帰的に処理します。~{はリストの中身について反復しますが、~@{の場合は引数が可変長引数のようになっている時に処理します。
  4. loadcompile-fileは拡張子を付ける必要はありませんが、付けても構いません。(load "filename")でも(load "filename.lisp")でも正しく処理されます。

Copyright (c) 2017-, satoshiweb.net. All rights reserved.