この記事はLisp Advent Calendar 2015 の10日目として書かれました。

ハッカーと画家で有名なポール・グレアム氏が、ジョン・マッカーシーの1960年のLispのオリジナル論文をCommon Lispで解説した記事を書いていることに最近になって気付いた。

かなり古い記事で(2002年)、内容はWikipediaの純LISPの項目と重なるところも多い。しかしながら現在でも非常に重要な記事なので、自分の勉強のためにも訳すことにした。別訳としてはlionfan氏による和訳があるが、こちらは前書き部分のみである。

この記事ではLispを一から定義して、evalを実装するところまでを解説している。ひとたびevalが実装されると、プログラムをあらゆる段階で評価できるようになるので、プログラムを変換してから評価したりできる。これは構文を新たに定義することに他ならないので、その上にどのような言語でも作ることができる。つまり、最も基本的な要素からメタプログラミングの土台を作るところまでを解説することが目的となっている。

この記事を理解できれば、他のどのような言語、計算機の上にも、自分自身でLispを実装できるようになる。一からプログラミング言語を作るという経験は非常に重要なので、ぜひ一度自分自身の手でLispを作ってみてほしい。

7つの原始オペレータ まず始めに式を定義する。式はアトムかリストのどちらかである。アトムは文字の並びである(例: foo )。リストは0個以上の式から成り、空白で区切られ、括弧でくくられている。ここでいくつか式の例を示す。 foo () ( foo ) ( foo bar ) ( a b ( c ) d ) 最後の式 (a b (c) d) は4つの要素を持つリストだが、3番目の要素 (c) はそれ自体が1つの要素を持つリストになっている。つまり、リストは入れ子にできる。 算術式 1 + 1 が値 2 を持つように、有効なLisp式もまた値を持つ。式eが値vを発生させるとき、それを「eがvを返す」と呼ぶ。式から値を取り出すことを評価と呼ぶ。例えば「式eを評価した結果、値vを得る」のように言う。 次のステップは、どのような種類の式がありうるか、そしてそれらがどのような値を返すかを定義することである。 ある式がリストであるとき、そのリストの最初の要素をオペレータと呼び、それ以降の要素を引数と呼ぶ。今から我々は7つの原始オペレータを定義していく。すなわち、 quote 、 atom 、 eq 、 car 、 cdr 、 cons 、そして cond の7つだ。これらは数学でいう公理のような役割を持つ。 1. quote (quote x) は x を返す。読みやすさのため、 (quote x) を 'x と略記することにする。 ( quote a ) ' a ( quote ( a b c )) 2. atom (atom x) は x がアトムまたは空リストのときにアトム t を返し、それ以外では空リスト () を返す。Lispでは慣習的に、真理値を表す際は、アトム t を真とし、空リストを偽とする。 ( atom ' a ) ( atom '( a b c )) ( atom '()) さて、今や我々はその引数が評価されるようなオペレータ atom を手に入れたので、 quote が何のために使われるのかを示すことができる。その目的とは、リストをquoteすることでそのリストを評価から守ることである。atomのようなオペレータに、quoteされてないリストが引数として与えられた場合、それはコードとして扱われる。 ( atom ( atom ' a )) 逆に、quoteされたリストは評価から守られるため、単にリストとして扱われる。したがって、次の場合は2要素のリストになり、atomによるテストの結果は偽になる。 ( atom '( atom 'a )) これはちょうど英語における引用符(quote)の使い方と似たようなものだ。単にCambridgeと書くとそれは「マサチューセッツ州の人口9万人の街」だが、引用符をつけて "Cambridge" と書くことで「9文字の英単語」になるのだ。つまり具体的な意味を持つ記述内容から表記上の文字列へと視点がシフトする。

他のプログラミング言語はquoteのような仕組みをほとんど持たないので、ちょっと異質な概念に思えるかもしれないが、それはLispが持つ最も独特な特徴の一つと密接に結びついている。その特徴とはすなわち、コードとデータがリストという同じデータ構造から出来ており、quoteオペレータがコードとデータを区別する方法であるということだ。 3. eq 比較のためのオペレータ。 (eq x y) は x と y の値が同じアトムか、両方とも空リストであるときに t を返し、それ以外のときは () を返す。 ( eq ' a ' a ) ( eq ' a ' b ) ( eq '() '()) 4. car (car x) は x の値がリストであるとして、そのリストの最初の要素を返す。 ( car '( a b c )) 5. cdr (cdr x) は x の値がリストであるとして、そのリストの最初の要素を除いた残りの全ての要素を返す。 ( cdr '( a b c )) 6. cons (cons x y) は y の値がリストであるとして、 x の値に y の値の要素が続くようなリストを返す。 ( cons ' a '( b c )) ( cons ' a ( cons ' b ( cons ' c '()))) ( car ( cons ' a '( b c ))) ( cdr ( cons ' a '( b c ))) 7. cond 条件分岐のためのオペレータ。 (cond (p1 e1) ... (pn en)) は以下のように評価される。 p1 から pn までの式が順番に評価されていき、各条件式 pi の値が t になった時点で、それと対応する ei 式が評価され、その値がcond式全体の値として返される。 ( cond (( eq ' a ' b ) ' first ) (( atom ' a ) ' second )) 7つの原始オペレータのうち、quoteとcondを除く5つではその引数は常に評価される*2。 この種のオペレータを関数(function)と呼ぶ。

関数の表記法 次に、新たな関数を記述するための記法を定義する。関数は (lambda (p1 ... pn) e) のように記述される。ここで p1 ... pn はそれぞれパラメータと呼ばれるアトムであり、 e は一つの式である。(各パラメータ pi は式 e の内部で参照できる) (( lambda ( p1 ... pn ) e ) a1 ... an ) のように、関数をリストの最初の要素に置く式を関数呼び出しと呼ぶ。その値は以下のように計算される。まず各引数 ai が評価される。次に、各パラメータ pi の値が対応する引数 ai の値に置き換えられ、それから関数本体部分 e が評価される。 (( lambda ( x ) ( cons x '( b ))) ' a ) (( lambda ( x y ) ( cons x ( cdr y ))) ' z '( a b c )) 次の式のように、 ( f a1 ... an ) 式の第一要素が原始オペレータでないアトム f で、fの値が関数 (lambda (p1 ... pn) e) であるなら、その式の値は (( lambda ( p1 ... pn ) e ) a1 ... an ) の値となる。言い替えるなら、パラメータは式の中で引数として使われるのと同様にオペレータとしても使われうる。例えば、次の式における f は、 'a をconsするという関数によって置き換えられ、オペレータとして '(b c) に対して作用する。 (( lambda ( f ) ( f '( b c ))) ( lambda ( x ) ( cons ' a x ))) (( lambda ( f ) ( funcall f '( b c ))) ( lambda ( x ) ( cons ' a x ))) これとはまた別に、関数が自分自身を参照できるようにする表記法があり、それによって再帰関数を定義するのが容易になる*3。 その記法は次のようなものだ。 ( label f ( lambda ( p1 ... pn ) e )) これは一つの関数を表しており、基本的には (lambda ( p1 ... pn ) e) のように振る舞うのだが、それに加えて、 f が関数本体 e の中であたかもこの関数のパラメータであるかのように現れたとき、そのfはこのlabel式自体へと評価されるという性質を持っている。 例えば、式 x 、アトム y 、リスト z を引数として取り、z中に(任意の深さで)出現するyをxに置き換えたリストを返す関数 (subst x y z) を定義したいとする。 ( subst ' m ' b '( a b ( a b c ) d )) この関数は以下のように表記できる。 ( label subst ( lambda ( x y z ) ( cond (( atom z ) ( cond (( eq z y ) x ) (' t z ))) (' t ( cons ( subst x y ( car z )) ( subst x y ( cdr z ))))))) ここで、 f = (label f (lambda ( p1 ... pn ) e)) を次のように略記する。 ( defun f ( p1 ... pn ) e ) そうすることで、substの定義はこうなる。(訳注： Common Lispにはこのlabel記法はないので、実際に動くのは以下のコードとなる。なお、substは組込み関数として既に定義されているため、 subst. と置き換えた。ただし、以下で定義しているevalの内部ではlabel記法を扱える) ( defun subst . ( x y z ) ( cond (( atom z ) ( cond (( eq z y ) x ) (' t z ))) (' t ( cons ( subst . x y ( car z )) ( subst . x y ( cdr z )))))) ちなみに、ここでcond式のデフォルト節をどうやって指定するかが出てきている。条件式が 't であるような節は常に成功する。従って、 ( cond ( x y ) (' t z )) は他の言語でいうところの if x then y else z と等価である。

いくつかの補助関数 ここまでに我々は関数の表記法を手に入れたので、7つの原始オペレータを使って、新しい関数を定義していくことにする。まずは、いくつかの共通するパターンに省略形を導入することで便利になるだろう。そこで、carとcdrの組合せに対応する省略形として cxxxr を使う。ここでxxxの部分はaかdの並びである。例えば、 (cadr e) は (car (cdr e)) の省略形であり、 e の二番目の要素を返す。 ( cadr '(( a b ) ( c d ) e )) ( caddr '(( a b ) ( c d ) e )) ( cdar '(( a b ) ( c d ) e )) また、 (cons e1 ... (cons en '()) ...) の省略形として (list e1 ... en) を使う。 ( cons ' a ( cons ' b ( cons ' c '()))) ( list ' a ' b ' c ) さらにいくつか新しい関数を定義するが、原始オペレータと区別するため、またCommon Lisp組込みの関数との名前衝突を回避するために、これらの関数の名前の最後にはピリオド . を付けておくことにする。 1. null. (null. x) はその引数 x が空リストかどうかをテストする。 ( defun null . ( x ) ( eq x '())) ( null . ' a ) ( null . '()) 2. and. (and. x y) は両方の引数が t を返すときに t を返し、それ以外の場合は () を返す。 ( defun and . ( x y ) ( cond ( x ( cond ( y ' t ) (' t '()))) (' t '()))) ( and . ( atom ' a ) ( eq ' a ' a )) ( and . ( atom ' a ) ( eq ' a ' b )) 3. not. (not. x) はその引数 x が () を返すときに t を返し、その引数 x が t を返すときに () を返す。 ( defun not . ( x ) ( cond ( x '()) (' t ' t ))) ( not ( eq ' a ' a )) ( not ( eq ' a ' b )) 4. append. (append. x y) は2つのリストを引数に取り、それらを連結したリストを返す。 ( defun append . ( x y ) ( cond (( null . x ) y ) (' t ( cons ( car x ) ( append . ( cdr x ) y ))))) ( append . '( a b ) '( c d )) ( append . '() '( c d )) 5. pair. (pair. x y) は2つの同じ長さのリストを引数に取って、それぞれのリストの対応する位置にある要素のペアからなるリストを返す。 ( defun pair. ( x y ) ( cond (( and . ( null . x ) ( null . y )) '()) (( and . ( not . ( atom x )) ( not . ( atom y ))) ( cons ( list ( car x ) ( car y )) ( pair. ( cdr x ) ( cdr y )))))) ( pair. '( x y z ) '( a b c )) 6. assoc. (assoc. x y) は1つのアトム x と、 pair. によって生成される形式のリスト y を引数に取って、yの要素のリストのうち、第一要素がxであるようなリストの第二要素を値として返す。 ( defun assoc . ( x y ) ( cond (( eq ( caar y ) x ) ( cadar y )) (' t ( assoc . x ( cdr y ))))) ( assoc . ' x '(( x a ) ( y b ))) ( assoc . ' x '(( x new ) ( x a ) ( y b )))