Lisp/始めの一歩/経験者用チュートリアル

この章では Lisp プログラムの構文について、いくつか理論的な基礎を教えています。
Lisp の操作は式に対して行われます。それぞれ Lisp の式はアトム( atom )か list の式かのどちらかです。アトムは、数値、文字列、シンボル、その他の構文です。 Lisp のシンボルは実に興味深いものです。 - シンボルについてはあとでまた別の項目で説明したいと思います。
Lisp がアトムか list かに見える式を評価しなければならなくなったとき、もしそれがアトムならその値が返されます。(数値、文字列、そしてその他のデータはそれ自身の値を返します。シンボルならばシンボルの値を返します。)もし、式が list ならば、 Lisp は list の最初の要素を探します。 list の最初の要素は car と呼ばれます。(古い用語の言い方で Contents of the Address part of Register を表します。)この list の car はシンボルか lambda 式でなければなりません。( lambda 式については後で論じます。)もし car がシンボルなら Lisp はその関数を抜き出します。(そのシンボルに関連付けられている関数であって、そのシンボルの値ではないことに注意です。)そして、 list の残りを抜き出したものを引数として関数を実行します。(もし list の残りの要素にさらに式が含まれるのならその式の内容も、これまでのプロセスと同じように評価されます。)
Example: (+ 1 2 3) は 6 を返します。シンボル "+" は関数 + と関連付けられており、引数の足し算を行います。 (+ 1 (+ 2 3) 4) は 10 を返します。第二引数に式が含まれているので、第二引数の式は外側の + に渡される前に評価されます。
+, -, *, / は数値に対する基本的な命令です。この関数ではいくつでも引数を引き受けることが出来ます。 (/ 1 2) は 0.5 ではなくて 1/2 となることを憶えておきましょう。 Lisp は分数が分かるのです。(複雑なものも同様にわかりますが) <, <=, = などの等号、不等号は数値の比較で使用されます。<, <=, = などの等号、不等号では複数の項目を引数に取れることを憶えておきましょう。
list は名前が示すように list を作成します。
cons はペアを作成します。 (ペアは二つの要素を持っています。 list では ない ということに気をつけてください。).
car あるいは first はその cons (ペア)の最初の要素を返します。 cdr あるいは rest はその cons の二番目の要素を返します。
上の例は下の例と同じ意味です。
list は Lisp においては非常に目立つので、 list が実際に何であるかは知っておいた方がよいでしょう。実は list は一つの例外を除いていくつもの cons から成り立っています。例外とは特別な list で nil と呼ばれるものです。 - nil は () としても知られています。 nil は 自己評価( self-evaluating ) のシンボルで、真偽値の偽の値を持つ定数として、そして空の list として利用されます。 nil は Lisp においての唯一の偽の値を持つものです。その他のものは if文 や同じような構文の用途としては真の値をもつことになります。 nil の反対の値は t です。これも自己評価するもので、真偽値の真の値を表します。しかし t は list ではありません。では list に戻りましょう。いわゆる 正しい list(正しくない list はここでは説明しません。)は nil かあるいは、 cdr が正しい list となる cons のどちらかだと定義されます。(ということは正しい list は一つの要素から始まる、ということを憶えておきましょう。 (cdr (cdr (cdr... (cdr x)))...) は有限個の list の cdr として nil となります。)
基本的に正しい list は直前の cons の cdr が次の cons となるような cons の連なりとなります。この説明ではわかりにくいでしょうから、 list がどのような構成になっているのか図で表示されたものを考えてみると理解しやすいでしょう。一つの cons は二つの正方形で仕切られた長方形で表すことが出来ます。それぞれの正方形は値を保持することが出来ます。正しい list では左の正方形はその list の要素を持ち、右の正方形は次の cons を保持します。(あるいは list の終わりを表すために、次の cons の代わりに nil を置きます。)それぞれの cons はちょうど list の一つの要素を持つことを憶えておきましょう。 (1 2 3) を図で表すと以下のようになります。
(1 2 3) は実は (1 . (2 . (3 . nil))) となります。以上の例に従えば、 (car (list 1 2 3)) は 1 となり、 (cdr (list 1 2 3)) は (2 3) となります。ただし (car nil) や (cdr nil) は nil となり以上の例が全く適用されません。これは全く首尾一貫したことではありません。なぜなら (cons nil nil) は nil と同じことではないからです。しかしこうすることによって便利になることが後々出てきます。
シンボルは、他のプログラミング言語での変数名と同じ役割を果たしています。基本的に、シンボルはいくつかの値と関連付けられた文字列です。普通の文字列は空白文字や、制御文字を含んだ、どんな文字からも構成することができます。しかし、ほとんどのシンボルではアルファベット文字、数字、そしてハイフン以外の文字は使いません。なぜなら他の文字は打ち込むには不便だからです。文字の "(", ")", "#", "\", ".", "|", ";" そして空白文字、ダブルクオーテーション、シングルクオーテーションマークを使用すると lisp のコードを読む人には読み間違えが起こりやすいのです。また、 "*" のようなほかの文字も慣習的に特定のプロセスにしか使われません。デフォルトでは Lisp は打ち込まれたアルファベット文字は大文字に変換します。
シンボルは使役されるものとして作られました。例えば、あなたが (setf x 1) と打ち込むと、シンボル "X" が作成されます。( Lisp はあなたが打ち込んだものを大文字に変換するということを覚えておいてください。)そしてこのシンボルには 1 という値が代入されます。シンボルをプログラムの中で使用する前にあらかじめ定義しておくことはいい習慣です。defvar や defparameter はこのような目的のために使用されます。
シンボルは名前と値の他に、関連付けられた設定値として、関数やクラスなどをとることができます。シンボルに関連付けられた関数を取得するにはスペシャルフォーム(スペシャルフォームについては次の章で論じます。)の function が使用されます。
Lisp には関数のような命令で、しかし少し違う振る舞いをするものがあります。それはマクロとスペシャルフォームといいます。関数は常に引数を評価しますが、しかし、たまにそれをやりたくない場合もあります。それでこの二つのフォームを実行する必要があるのです。
たとえば、あらゆるところで目にする if 構文について考えてみましょう。 if 構文は ( if condition then else )という形式を持ちます。condition(条件)が最初に評価され、もし condition(条件) が nil でないなら、 then(それなら) の部分が実行され、あるいは条件部分が nil なら else(さもなければ) を実行という風に条件の中身によって分岐します。したがって、 (if t 1 2) は 1 を返しますし、 (if nil 1 2) は 2 を返すことになります。明らかに、 if 構文は関数として使用されていません。なぜなら、最後の二つの引数の内の一つしか評価されないからです。そのため、この構文が 25 あるスペシャルフォームのうちの一つとして作られ、 Lisp の実装に組み込まれたのです。
他のスペシャルフォームに quote があります。 quote は唯一の引数を返し、評価されないものにします。重ねて言いますが、関数はいつも引数を評価してしまうので、こういうことは関数には不可能です。 Quote は非常に頻繁に使用されます。そのため記述を短縮するために一文字の ' で表すことが出来ます。ですから (quote x) は 'x と同様の意味を持ちます。 quote は簡便に list を作成するために使用されます。たとえば、 '(1 2 3) は (1 2 3) を返しますし、 '(x y z) は (x y z) を返します。これを (list x y z) と比べてみると、 (list x y z) の場合は x, y, z の値の list を作成し、もし値が代入されていなければエラーを返すのに対して、実際には '(x y z) は (list 'x 'y 'z) の値と同じ意味になります。
マクロはスペシャルフォームのようではあるのですが、 Lisp の実装においては変更不能なように記述されていません。その代わりに、マクロを Lisp のコードで定義することが可能になっています。あなたが使うことになる多くの Lisp の構文は実際にはマクロなのです。非常に本質的な構文だけが変更不能なように実装されています。もちろんユーザーにとってはそこには何の違いもないのですが。
この章では単純な課題を Lisp でどのように扱うか説明します。たくさんの役に立つ構文を紹介するので、この章を読んだ後には、簡単なプログラムを書けるようになっているはずです。
変数に値を代入することは多くのプログラミング言語において重要なプロセスですが、 Lisp においては非常にまれです。 Lisp はマルチパラダイム言語なので、しばしば関数型言語に言及しますし、関数型言語としてもプログラムされています。関数型言語で許されていない(あるいは少なくとも推奨されてはいない)のが、「状態」の使用と、記憶された情報を明示的に変更する関数の挙動です。理論的には純粋関数型言語では代入は決して必要ありません。気付かれたかもしれませんが、ここまでの章で "シンボル" を説明している章を除いてはどんな値も代入していません。記憶されたグローバルな値はほとんど必要ないことがわかります。
しかし、言及したように、それでも値の代入は便利です。そのため Lisp では値の代入機能も提供してくれています。 setf と setq マクロはシンボルに値を代入します。
Setf は setq よりも強力です。 setf ではプログラマが変数の一部を変更することが可能なのです。
このため、 setf は setq よりも頻繁に利用されます。
他の言語では、代入は x=1 のように記述されます。ここでの = は数学における = 記号とは意味が違います。しかし Lisp では = 記号は、たとえば、数値の比較を試すときのような数学的な定義のために用意されています。 Lisp の代入における (setf place value) という記述は期待していたものよりも複雑に見えるかもしれませんが、この機能は拡張可能で、代入された内部表現の削除がユーザーに許されている、ということを憶えておけば役に立つでしょう。これは他の言語においては = 演算子に再割り当てを行うのに似ています。(重要なもの再割り当てすることはは C言語 や他のプログラミング言語では可能ではありませんが)
Setf は余計なキーストロークが必要になりますが、用途は役に立ちます。しかし、実際には他の言語に比べて代入を使うことはあまりないでしょう。というのは Lisp においては値を記憶するほかの方法があるからです。それが bind です。次の章で説明します。
メモ: setq は set quote を表したものです。もともと、 set 関数はその最初の引数を評価するものですが、プログラマはいちいち引数を quote しているとうんざりしてくるので、 setq を set quote の特別な命令としたのです。 Set は今は非推奨となっています。
値がシンボルに bind(束縛) されたとき、値は一時的に保持されるだけで、その後解放され、 bind された値は忘れられてしまいます。 let か let* を使用すれば、プログラムの一部分で複数の値を複数の変数に bind することが出来ます。 let と let* の違いは、 let は同時に変数を初期化するのに対して、 let* は初期化を連続して行うということです。下の例が一見してわかりやすいでしょう。
let の定義部分では変数を実際のシンボルのように定義して使用することができます。 - let の外側では内側で定義されたシンボルは解放されますし、またあるいは、外側で定義された全く別の値を持つシンボルかもしれません。もし let の定義部分で関数を呼び出すか、あるいは関数を定義するかをすると、その let 内の bind はいくつか興味深い相互作用をもたらします。(この作用についてはこのマニュアルの範囲を超えるので説明はしません。)これらの変数を setf で使用しても、 let 内で格納される新しい値は一時的なものです。 let の定義部分の最後の部分が実行されるとすぐに結果が返り、変数は元々の値に復帰します。以下の例で示してみましょう。
良いプログラミングの習慣として、一般的には可能ならばローカル変数を利用するのが良いとされます。そして限定的なグローバル変数は、どうしても必要な場合に限った方がよいとされます。ですから、可能な場合は let を使用し、 setf を扱うときは使用するたびに重い負担がかかるぐらいの認識をすべきです。
if 命令文は既に以前に説明しましたが、制御構造という点では難しいように思えるかもしれません。なぜなら if 文は真偽のそれぞれの分岐を一つずつしか用意していないからです。これでは多くの状況で不便が生じます。しかし喜ばしいことに Lisp の文法では C言語 の波括弧や、 Pascal の begin/end よりも自由にプログラムのブロックを定義できます。 progn では非常に単純なコードのブロックを作成できます。引数を一つ一つ実行し、最後の一つを結果として返します。
let と let* もこの目的のために使用されます。特に、分岐の中で一時的な値が必要な場合などです。
block は命名されたコードのブロックを作成します。 block では return-from で値を返すことが出来ます。
他にも the, locally, prog1, tagbody など様々なフォームが存在します。幸いなことに、もしあなたが Lisp のマクロを書くのでない限り、 if が block を使わなければならないような箇所での唯一の構文になるでしょう。
if は何度も繰り返し使うと非常に汚く見えるコードになるので、ifに代わった便利で適切なマクロがいくつもあります。そのため、 if を常態的に何度も使うことのないようにしましょう。when は最初の引数を評価し、もしそれが nil でないなら、残りの引数を評価し、最後の評価を返します。unless はもし最初の引数が nil なら残りの引数を評価します。 when も unless も最初の引数の条件がそれぞれ満たされないなら nil を返します。
cond はもう少し複雑ですが、より便利です。 cond では複数の条件を設定することができ、条件が nil ではないものに出会うまで条件をテストします。そして、条件節と関連したコードが評価されます。この方が、いくつも入れ子状になった if 文よりも構文解析が簡単で、見た目がきれいです。cond の文法は以下のようになります。
case は cond と似ていますが、引数の値が確かめられてから分岐が実行されるところが違います。
or では引数の一つが nil でないものに出会うまで引数を評価します。nil でない場合の値を返すか、あるいは最後に評価した値を返します。
and では引数のひとつが nil のものに出会うまで引数を評価します。 nil だった場合の値を返すか、あるいは最後に評価した値を返します。
もしかしたらここまで or と and が論理演算子として同様に使うこともできることに気づかなかったかもしれません。ともかくも、 nil でないものは全て true なのだ、ということを憶えておきましょう。
繰り返し( Loops )、あるいは式を何度も評価することは、繰り返しを評価するための様々な手段で扱われています。最も便利な方法は loop です。 loop の単純な構文では、実行部分は return 命令が呼び出されるまでシンプルな実行が繰り返されます。return が呼び出されると特定の値が返ります。
より複雑な形の loop の構文は実例を参考にしながら学んだほうが良いでしょう。行いたい繰り返しの種類によっては、 for や until などの命令が使用されます。 loop はあらゆる種類の繰り返しとして十分には違いありませんが、 loop の文法を完全に身に付けたくない人のために、また他の構文が存在します。
dotimes はコードの繰り返しを特定の回数行います。 dotimes の文法は以下のようになります。
dotimes は var での値を、 0 から number まで一つずつ繰り返しの毎に増加させます。そして、繰り返し毎に変化する var とともに form の内容を実行し、最後には result の内容を返します。
dolist は list を通して繰り返しを行います。 dolist の文法は dotimes の number が list に入れ替わっているのを除きほぼ同じです。 dotimes における var は dolist では list の car で始まり、 list の最後の要素になるまで繰り返しが行われます。
mapcar では引数の異なる組み合わせに、それぞれ関数を使用し、結果を list にまとめて返します。例えば、以下のようにです。
上の#'+ は、 (function +) の短縮形です。関数 + はまず引数を list にした (1 2 3) に適用され、それから (3 4 5) そして (6 7 8) に適用され終了します。
関数は defun マクロを使用して定義します。
作成された関数は function-name のシンボルと関連付けられます。そして他のどんな関数からでも呼び出すことが出来るようになります。この方法で定義される関数は再帰になることも、お互いに呼び出すことも可能だということには言及しておかなくてはなりません。これはほとんどの Lisp プログラミングの本質的な部分だからです。再帰関数の例を以下に示します。
一見したように、この関数では、別のやり方では複雑な操作を、自分自身を繰り返し呼び出すことでシンプルに記述しています。このプロセスは x が 0 になったときに止まります。 x は呼び出されるごとに減少していきます。これは x が正の整数のままで、無限ループになる可能性を除去するためです。引数となっている x は関数で呼び出されるごとに値が違うことに注意をしましょう。上の "Bind" の項目で見たように、これらの値は以前の物に上書きされるわけではありません。
特殊な命令 lambda は一回限りの使用に使うような無名関数を作成します。 lambda の書き方は defun で "defun function-name" とされている部分を、 "lambda" とする以外は defun と同じです。この関数は再帰を行うことが出来ません。ほとんどの場合、 lambda 関数は mapcar や funcall, apply などの関数フォームの一部として使用されます。 lambda を利用することで過度の繰り返しや、過度のメモリ消費を排除するためです。
flet や labels を使用することで関数を一時的に bind することができます。flet や labels は let や let* と非常によく似ています。これらの違いは labels は自身の関数を参照できるのに対して、一方で flet は以前に定義した関数を参照できるに過ぎません。
引数 a1, a2, a3 を持つ関数 f を呼び出すには以下のようにします。
しばしば、変数に入った事前に名前のわからない関数を呼び出さなければならない時があります。あるいは、もしかしたら、いくつの引数を渡せばいいのか、わからないこともあるかもしれません。そのような場合でも funcall や apply が役に立ちます。他の全ての関数と同じように funcall も apply も自身に渡された引数を評価します。最初の引数は呼び出される関数を作り出すもので、残りの引数は最初の引数で作成された関数に渡される引数になります。funcall はどんな引数が渡されても指定した関数をただ呼び出すだけです。 Apply では最後の引数が list かどうかチェックを行い、もし list ならば、引数全体が一つの list であるように扱います。以下の例でそれぞれ比べてみましょう。
このチュートリアルでは単純な課題の入力と出力した扱ってきませんでしたが、ユーザーからの入力を読み取るには read を使用します。 read では入力された任意の Lisp 式を読み取ろうと試み、その式を読み込んだ結果の値を返します。
上の例では、返された値は三つの要素を持った list です。シンボル + 、数値 1 、そしてシンボル X です。(大文字に変換されていることに注意です。) read は read-eval-print loop で構成される三つの関数のうちの一つで、 Lisp の中心となる要素の一つです。これはあなたが Lisp のプロンプトに打ち込んだ式を読み込むのに使われている関数と同じものです。
read が数値や list をユーザーから受け取るのに便利な一方で、大抵のユーザーはコンピューターに別の方法で送り込む また別のデータの型を求めるでしょう。文字列を例にとってみましょう。 read に文字列を認識させるにはダブルクオーテーションで "このように" 文字列を囲わなければなりません。しかし、一般のユーザーは このように クオートで囲わずにただ打ち込んで Enter を押したいはずです。そこで一般的な目的のための関数が入力のために使われます。 read-line です。 read-line はユーザーが Enter を押す前に打ち込んだものをなんでも文字列として返します。これであなたはあなたの望む情報を抜き出すための文字列を処理できるのです。
データ出力のためには本当にたくさんの関数が利用可能です。その中でも特に興味深いのが princ です。この関数はシンプルに与えられた値を打ち出し、そしてその値を返します。おそらく Lisp コンソールで初めてこれを使用したら困惑するのではないでしょうか。
最初のクオートされていない aaa は打ち出されたものです。一方で次の "aaa" は princ によって返された値です。(print 関数ではこれが打ち出される形式です。あまり見栄えがいいものではありません。)他に役に立ちそうな関数は terpri です。この関数は標準出力に改行を出力します。(名前の "terpri" は歴史的な経緯によるもので、 "TERminate PRInt line" を意味します。)
詳細はリファレンスに依ってください。