2010年4月26日月曜日

#9LISP 014 メモ

#9LISP はいよいよマクロを通してCommon Lispに突入するそうです。ぶっちゃけ「良かったな」と(笑)。


@valvallowさんと、


「LOLを参考にして、Forth実装を通してPostScriptがSchemeで実装出来たらいいね。」

とか言ってたんですが。無茶苦茶メンド臭い。ハッキリ言って「無理じゃね?」とか思って来てました。多分僕はヘタレなのでしょう。ええ、間違いなく。それは否定しない。


まあ、でも多分一番問題なのは、Schemeには標準仕様としてmacroexpandが定義されていないに尽きると思います。手探りでマクロ展開形を想像しながらやる、ってのはシャレにならんのですよ。CLerが


「Schemeの仕様は貧弱だ」

と言う批判をするのは、この辺に付いては妥当だと思います。マクロ書くのに展開形が見れない、ってのはLisp系言語仕様設計としてはポイントがズレまくってます。困ったもんだ。


そんなわけで、「CLで伝統的マクロやる?万歳!」とか思ったんですね。率直な感想です。



※1: もっとも「あらゆる実装でmacroexpandが無い」と言う気はありません。むしろ実装依存ならあるって言った方が正しいです。

が。こう言う重要な機能が実装依存だ、ってのがぶっちゃけ納得出来ません。話によれば衛生的マクロのmacroexpandは実装が難しい、と言う話なんですが、だったら片手落ちにも程があるだろ、と。いや、納得せんぞ(笑)。その辺実装者側に投げて良い、って事もねえだろ、と。

Gaucheにはmacroexpandがあって、Guileはザーっとマニュアル見た限り存在せず、PLT Schemeはワケワカメです。

※2: 余談ですが、PLT のメーリングリストにも「PLTにはmacroexpandがないの?」ってトピックがあって、回答者側の一人が

    「DrSchemeのMacro Stepper使ってみろよ?macroexpandなんて使う気にならなくなるから。」

とかとても能天気な事を書いています(笑)。

実際、狙ったマクロ「だけ」を展開出来て、一見優れものに見えるんですが、

  • 実はPLTのメイン処理系であるMzSchemeでは動かない。Emacsで使う場合はわざわざMzSchemeを止めて、もう一つの処理系MrEdを呼び出さないと使えない。

  • その上、一々ライブラリとして(require macro-debugger/expand)しないといけない。

  • 構文が

    (syntax->datum
    (expand-only #'展開したい式
    (list #'展開したいマクロ名<複数列挙可>))))

    と打つのがメンド臭い程長い。


と三重苦です。マジな話やってられません


Common Lisp実装


んで、次回までANSI Common Lisp処理系を入れようと言う話です。 #9LISP で話に上がったANSI CL処理系は次の4つ。メモしておきます。



  • CMUCL: 米国カーネギー・メロン大学で開発されたANSI Common Lisp処理系(だからそのままCarnegie Mellon University Common Lisp)。ただし、現時点での開発継続は有志による筈。LOL一押しの処理系。CLのフリー実装としては1、2位を誇る処理速度を叩きだし、同じくフリー処理系のSBCL(後述)や商用のScieneer Common Lispのルーツである。

    ただし、基本的にWindowsはサポートしていない、UNIX用に特化した処理系と考えて良い。また、UNICODEもサポートしていない。

  • SBCL: CMUCLから友好的にフォークしたフリーのANSI Common Lisp処理系。CMUCLに独自に拡張を加えたもの、として考えて良い。CMUCLもSBCLもコンパイラはPythonと呼ばれるネイティヴコンパイラを使用している(LLのPythonとは無関係で、CMUCL/SBCLのユーザーは「あっちが後だ!」とたまに話を蒸し返す・笑)。

    最大の特徴はCMUCLと違い、UNICODEが扱える事。従って、次のようなコードを書いても良い。

    CL-USER> (defun のりピー (symb)
    (eq symb '酒井法子))
    のりピー
    CL-USER> (のりピー '酒井法子)
    T
    CL-USER> (のりピー '小倉優子)
    NIL
    CL-USER>

    日本語が扱えるフリーの最速処理系だと思われる。また、実験的だがWindows版も提供している。

    なお、SBCLはSteel Bank Common Lisp(鉄鋼-銀行 Common Lisp)の意。カーネギー・メロン大学の創設者の一人、アンドリュー・カーネギーが鉄鋼で財を成し、もう一人のアンドリュー・メロンは銀行業で財を成した事に由来する。

  • GNU CLISP: ドイツ製ANSI Common Lispのフリー処理系。1987年にATARI STで開発されたものが初版。readlineの使用に関してFSFのリチャード・ストールマンと揉め、後にGNUに寄贈される。

    特徴は、ソースのコンパイルを行うと、ネイティヴ・コードではなく、バイト・コードへとコンパイルする。故に実行速度よりオブジェクトコードの移植性を重視した実装となっている。スピードはCMUCL/SBCLに劣るが、竹内関数でのベンチマークを試してみると、コンパイルされたコードの実行速度はScheme実装Gaucheとさほど変わらなかった。故に、SchemeのGaucheが速い、と感じるなら、問題は特に無いと思われる。また、UNICODEにも対応している。

    WindowsだろうとUNIXだろうとどこでも動くので、世界で一番使われているフリー実装だと思われる。また、ポール・グレアムが作ったVia WebはCLISPを使っていた。Arcの最初の実装もCLISPで書いたらしい。

    開発はまあまあ活発で、ヴァージョンアップは大体定期的で、一年に一回くらい。

  • ABCL: Armed Bear Common Lisp。JavaによるANSI Common Lispのフリー実装。開発が活発なのかそうじゃないのか、いまいち良く分からない。

    ちなみに一時期、Ubuntu 7.04~7.10辺りのレポジトリに入っていたが、今は消えてしまった。人気がないのか?良く分からん実装である。


とまあ、主観交えて書けばこんな感じでしょうか。お勧めはPC-UNIXだったらSBCL、Windowsだったら素直にCLISPにしとけば基本問題無い、と思います。Macは良く知らん。


どの実装を選ぶか、ってのは頭が痛い問題なんで、ぶっちゃけ、オールインワンのLispbox入れときゃ充分なんじゃねえの?とか思います。その方が設定で面倒臭い思いしなくて済みますしね。



















































OS X (10.4/PPC) OS X (10.4/Intel) GNU/Linux x86 GNU/Linux x86-64 Windows
Allegro 8.1 8.1 8.1 8.1 8.1
SBCL 0.9.7 0.9.7
Clozure CL 1.0 1.0
CLISP 2.35 2.36 2.37


Common LispとSchemeの大まかな違い


SchemeはLisp-1、Common LispはLisp-2である


多分 @aharisu さんだったと思いますけど「Common LispとSchemeの違いって何?」と言う質問に対して、


「SchemeはLisp-1だけどCommon LispはLisp-2です」

って即答してました。多分一言で言うとそうだろうと思います。両者ともレキシカル・スコープを採用したLispですが、そうなると一言で言える違いはコレになるでしょう。もちろん、細かい話では色々な違いが出てきますが。



Lisp-2は名前空間を二つ持ち、片方を演算子に使い片方を変数に使う。一方、Lisp-1の名前空間は1つだ。


実際、Common Lispの方は名前、つまりシンボルの衝突を避ける為に神経質なまでに繊細な設計を行っています。逆に言うと、設計が繊細なお陰でユーザーはあまり面倒臭い事を気にかける必要がありません。大雑把にいられる。


例えば、このページで紹介されてる例が良い例だと思います。



;;; Common Lisp の場合
CL-USER> (setf sin 1.0)
;
; caught WARNING:
; undefined variable: SIN
;
; compilation unit finished
; Undefined variable:
; SIN
; caught 1 WARNING condition
1.0
CL-USER> (sin sin)
0.84147096
CL-USER>

;;; Schemeの場合

> (define sin 1.0)
> (sin sin)
procedure application: expected procedure, given: 1.0; arguments were: 1.0

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

SBCLだと警告が出ますが、それでも(sin sin)なんてヘンな式でもへーキで実行してしまいます。S式の第一要素は関数、第二要素は変数だ、と賢く解釈してくれるから、です。一方Schemeはそうしてくれません。何故なら、sinを1.0で束縛してしまった以上、sinはもはや手続きじゃなくなってしまったのです。



シンボル



上のsinみたいなのをシンボルと呼ぶわけですが。Lispの特徴としてはこのシンボルがデータ型であると言うのが前提なんですが、結局このシンボルの扱いがCommon LispとSchemeでは大きく違うわけです。


Schemeの仕様書(R5RS)には次のように定義されています。



シンボルとは,二つのシンボルが(eqv? の意味で) 同一なのは名前が同じようにつづられるときかつそのときに限られるという事実に,その有用性がかかっているオブジェクトである。これはまさに,プログラムで識別子を表現するために必要とされる性質であり,したがってScheme の大多数の実装はシンボルを内部的にその目的のために利用している。シンボルは他の多くの応用にも有用である。たとえば,列挙値をPascal で利用するのと同じ用途に,シンボルを利用してもよい。

色々ゴチャゴチャと書いていますが、要するに、


  1. 各シンボルはプログラム中で「唯一無二の存在」でなければならない。

  2. じゃないと識別子として役に立たない。

  3. つまり、シンボルは「名前を識別する為だけに」存在する。


と言ってるんですね。言わば当たり前の事を言ってるんですが。他の言語鑑みてもわざわざシンボル型なんてデータ型が存在しなくても良いくらいアッサリしています。


ところが、Common Lispの場合、シンボル自体に機能がある。詳しい事はCLHSを見てほしいんですが、端的な表現としてはシンボルに対してアクセサが3つも定義されている。C的に言うとまるで、シンボルは構造体で定義されているデータ型の如し、です。



例えば上のように(setf sin 1.0)とREPL上で評価した後、次のように、



CL-USER> (symbol-function 'sin)
#<FUNCTION SIN>
CL-USER>

とすればsinと言う組み込み関数が束縛されている事が分かります。


続いて次のようにすれば、


CL-USER> (symbol-value 'sin)
1.0
CL-USER>

sinに1.0が束縛されている事が分かります。つまり同一のsinと言うシンボルが二つの役割を担っていると言う事が明らさまに分かる。要するに、名前空間が二つ存在しているんです。


んじゃあ、最後のsymbol-plistってのは何なのか?本筋にあんま関係ないんで端折って書いておくと、シンボルはついでに属性リストと言う特別なリストを「内部構造として」持つことが可能なんです。symbol-plistはそいつをリテラルとして引っ張り出すアクセサです。


例えば大域変数として次のようなメンバー名のリストとする9lispを定義します。



CL-USER> (defparameter 9lisp '(valvallow mutronix 堀 田中克之 そーり shunsuk gibson cametan sakurako_s _Relm koki-h AKIRA))
9LISP
CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER>

シンボルを大域変数で定義した場合、REPLでシンボルを評価した結果とsymbol-valueでシンボルにアクセスした結果は同じです。



CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER> (symbol-value '9lisp)
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER>

ところが、シンボル9lispはここで定義したリストと別の属性リスト(キーとデータが交互に並んだリストの事)を内部に持つことが出来ます。



CL-USER> (setf (get '9lisp :parent-group) 'KPF
(get '9lisp :通称) '9lisp
(get '9lisp :explanation) '九州熊本を中心にLISPの勉強会を開催しています
(get '9lisp :開催) '隔週
(get '9lisp :オンライン参加) t)
T
CL-USER> (symbol-plist '9lisp)
(:オンライン参加 T :開催 隔週 :EXPLANATION 九州熊本を中心にLISPの勉強会を開催しています :通称 9LISP
:PARENT-GROUP KPF)
CL-USER>

そして、先ほど大域変数として値を与えた9lispとその内部構造である属性リストは、当然ながら名前を共有しながら全く別物なんです。



CL-USER> 9lisp
(VALVALLOW MUTRONIX 堀 田中克之 そーり SHUNSUK GIBSON CAMETAN SAKURAKO_S _RELM KOKI-H
AKIRA)
CL-USER> (symbol-plist '9lisp)
(:オンライン参加 T :開催 隔週 :EXPLANATION 九州熊本を中心にLISPの勉強会を開催しています :通称 9LISP
:PARENT-GROUP KPF)
CL-USER> (equal 9lisp (symbol-plist '9lisp))
NIL
CL-USER>

とまあ、Common Lispに於いてはシンボルは最低でも3つの役割を担っています。ただし、最後の属性リストに関して言うと、ポール・グレアムは、


なお、Common Lispでは属性リストはあまり使われない。ハッシュ表を使うことがはるかに多い。



と記述しています。と言うわけで、属性リストは使わなくても構わないらしいですが、同時にここではANSI Common LispにはSchemeでは存在しないハッシュ表も存在すると言う事が分かります。



いずれにせよ、同一のシンボルに対して最低でも3つの役割を持たせているのがANSI Common Lispで、これだけに限らず、たとえ同一のシンボルが存在しようと可能な限り衝突を避けようと設計されているのがANSI Common Lispの仕様です。ここでは立ち入りませんが、パッケージと言う仕組みがあって、何重にもシンボル衝突を回避しようとしてる。そしてそれが実はANSI Common Lispの設計上の肝なんです。


比較するとSchemeのシンボルはANSI Common Lispに比べるとお粗末、と言えばお粗末です。黒板の人の



(quote a) と入れたら A と返ってくるのが symbol なわけではなくて、 symbol っちゅうものは、そもそもプログラマから見れば first class object であって、 packageintern したり unintern したり、他の package から inherit したり、 shadow したり、 export したり import したりできるもの。それが symbol です。
Scheme には、この symbol がありません。


と言うScheme批判はこの辺なんですよね。少なくとも今まで見た通り、Schemeのシンボルだと出来る事は殆ど無いに等しい、と言う事ですから。



写像関数と関数適用の例


さて、名前空間が二つある、と言う事はシンボルが表すものが最低2つはある、と言う事です。その辺でSchemeと若干作法が変わってきたりします。代表的なトコで写像関数を見てみましょう。


Schemeでは写像手続きはmapですが、ANSI Common Lispでそれに一番近いものはmapcarでしょう。


そこで次のような問題を考えます。



要素がリストであるリストがある。各要素のcarを取ったリストを返せ。

Schemeだったらこう書くでしょう。



> (map car '((Google YouTube) (ニワンゴ ニコニコ動画)))
(Google ニワンゴ)
>

ところが、これがCommon Lispじゃあ上手く動かない。



CL-USER> (mapcar car '((Google YouTube) (ニワンゴ ニコニコ動画)))
The variable CAR is unbound.
[Condition of type UNBOUND-VARIABLE]
; Evaluation aborted.
CL-USER>

何と「変数carは未束縛です」と文句を言ってくる。これは何故かというと、ANSI Common LispではS式の第一要素に置かれたシンボルに付いては素直に「関数」だと解釈してくれるんですが、第二要素以降に付いてはデフォルトでは必ず「変数」だ、と解釈してくるんです。一方、Lisp-1であるSchemeでは、S式のどこに手続きとして定義されたシンボルを置いても、手続きは手続き、なわけです。この辺がCommon LispとSchemeでは食い違ってくるんです。


そこで、ANSI Common Lispでは、S式の第二要素以降に置いたシンボルが変数なのか関数なのか、処理系に知らせてやらないとならない。上の問題はANSI Common Lispではこう書きます。



CL-USER> (mapcar #'car '((Google YouTube) (ニワンゴ ニコニコ動画)))
(GOOGLE ニワンゴ)
CL-USER>

carの前に置いた記号はそのままシャープクオートと呼びます。これ、実際は省略記法で、@valvallowさんに「何の省略記号だっけ?」と訊かれてぶっちゃけ忘れてたんですけど(笑)、functionと言う特殊オペレータ(Schemeで言う特殊形式)の省略記法ですね。故に上の式は次と等価です。



CL-USER> (mapcar (function car) '((Google YouTube) (ニワンゴ ニコニコ動画)))
(GOOGLE ニワンゴ)
CL-USER>

実際(function 何とか)とか書いてられねえけどな(笑)。メンドっちくて(笑)。だから#'だけ覚えていてその意味忘れてた僕は責められないと思う(笑)。多分(笑)。


いずれにせよ、ANSI Common Lispでは、写像関数を含む高階関数が関数引数を取る場合、渡す関数引数には#'を付ける、と覚えておきましょう。



ANSI Common Lispでの関数定義



実際Schemeは変わった言語で、事実上、例えばPascal的な意味での「手続き定義」の様式を持ちません。特殊形式defineが行ってるのはシンボルとデータ(つや~な言い方をするとオブジェクト)を結びつけてる(束縛)してるだけ、です。当然λ式で形作られるクロージャもデータなんで、全ての「値」を一元的に扱える、と言うのがSchemeの特徴なわけです。



> (define a 1) ;シンボル a と数値 1 を結びつけた例
> a
1
> (define fact ;シンボル fact と クロージャを結び付けた例
(lambda (n) ;これが Scheme に於ける手続き定義の基本となる
(letrec ((iter
(lambda (m acc)
(if (zero? m)
acc
(iter (- m 1) (* m acc))))))
(iter n 1))))
> fact
#<procedure:fact> ;Scheme に於ける手続きリテラルの例
>

一方、Lisp-2であるANSI Common Lispの場合、シンボルには複数の役割がある、と言う事を既に見ました。従って、表層的にせよ、変数定義と関数定義は分かれてなければならないと言う要請が出てきます(※)。ANSI Common Lispで用いられる関数定義の為のマクロがdefunです。



CL-USER> (defun fact (n)
(labels ((iter (m acc)
(if (zerop m)
acc
(iter (1- m) (* m acc)))))
(iter n 1)))
FACT
CL-USER> (symbol-function 'fact)
#<FUNCTION FACT>
CL-USER>

一見、SchemeのMIT記法による手続き定義の構文糖衣に似てますね。並べて比べてみれば分かると思います。











ANSI Common Lisp の関数定義の例 Scheme の手続き定義の例


(defun fact (n)
(labels ((iter (m acc)
(if (zerop m)
acc
(iter (1- m) (* m acc)))))
(iter n 1)))



(define (fact n)
(letrec ((iter (lambda (m acc)
(if (zero? m)
acc
(fact (- m 1) (* m acc))))))
(iter n 1)))


パラメータリスト(引数リスト)に関数名/手続き名を表すシンボルが第一要素として含まれるか否か、って辺りだけが違います。ANSI Common Lispの場合は含まない、SchemeのMIT型構文糖衣だと含む、と言うのが違いです。


実際問題、SchemeとCommon Lisp行き来してると、この辺良く間違うんですよ(笑)。手癖と言うか(笑)。関数定義/手続き定義をやってエラーが出て、一瞬、


「あれ?どこ間違えたんだろ?」

ってクダラないトコで悩む事が良くあります(笑)。僕だけか(笑)?



※: 本当は出てきません。あくまでユーザー的に表層では、と言う事です。


Common Lispの大域変数定義


Lisp-1とLisp-2に絞って大まかなSchemeとANSI Common Lispの違いを見ると、残るは大域変数だけ、です。が、これがややこしいんで端折ります。


Schemeの場合、結局何でもdefineなんですが、ANSI Common Lispの場合は大域変数定義には次の3つがあるんです。



んで、これらの使い分け、ってのが細かくてぶっちゃけピンと来ていません(笑)。実際僕だけでもないみたいで、defconstantはともかくとして、defvar派とかdefparameter派、みたいなのがいたりする模様です(※)。


ザーッと手元の資料見てみても、割に実践Common Lispが一番詳細に解説してたりしますね。が、長いんで転載出来ません(笑)。英語で良ければ原書の第6章辺りを参考にしてください。



※:本によって解説が偏ってる、ってのは事実。例えばポール・グレアムは「自分の好きな機能」に関してはページを多く割く傾向があって、defvarはあんまり使わないんだけどdefparameterは良く使う、ってのが見て取れる。他の本も大同小異だったりする。

CLerの間でこの手の「バカに豊富な機能のどれを使うか?」と言うので意見が分かれる事が良くあるようで、例えば簡単なトコでは等価述語であるeqeqlに対しても「何でもeq派」と「何でもeql派」に分かれているらしい。前者はスピード重視派、後者はデフォルト推奨派、ならしい。

ちなみに、Emacs Lispにはdefparameterdefconstantは存在しない模様でビックリした。恐らくこの中ではdefvarが歴史的には一番由緒正しいのだろう。


Common LispとSchemeのちょっとだけ細かい違い



ではもうちょっとだけANSI Common LispとSchemeの違いを見てみましょう。これらはtrivialな話ではなくって、実際SchemeからはじめてCommon Lispへと進んでプログラムを書く場合、場合によっては障害となるんじゃないか、と言う個人的な観点に絞ります。



ANSI Common Lispはどんな形であれ値を返す


Schemeの仕様書を見ていくと、意外と「返り値は未定義である」と言う表現を多く見かけます。それが結構多くて困った事が多いんですが(実際多い)、一方、ANSI Common Lispは、一見無意味に見えるものでも必ず値を返します。代表的なところは出力関数でしょうか。












Common Lisp の format 関数の例

Scheme の display 手続きの例


CL-USER> (format t "~A~%" "Hello, World!")
Hello, World! ;表示
NIL ;format の返り値自体は nil
CL-USER>



> (display "Hello, World!\n")
Hello, World! ;Hello, World! は表示だが、display の返り値は未定義
>


一見無意味に見えるんですが、Common Lispの出力関数formatは返り値があります。一方、Schemeのdisplayの返り値は未定義です。


「どんな場合でも何らかの値を返す」と言う事の信頼性は重要です。ANSI Common Lispに関して言うと、信頼して構わない、と言う事です。



ANSI Common Lispの真偽値


ANSI Common Lispでの真値はt、偽値はnilで表され、Schemeではそれぞれ#t#fで表されます。


これだけ見ると大した違いが無いように見えるんですが、どころがどっこい。ANSI Common Lispでは次のようなルールが存在します。



  • 空リストはnilの事である。

  • nilはユニークなシンボルである。

  • 論理上、nilでないものは、全てtである。


従ってANSI Common LispとSchemeでは次のような食い違いが起こります。











ANSI Common Lispの場合 Schemeの場合


CL-USER> (null '())
T
CL-USER> (not '())
T
CL-USER>


> (null? '())
#t
> (not '())
#f
>

実はANSI Common Lispではnullnotは同機能異名の関数なんですが、Schemeではnull?は空リスト判定用述語、notはあくまで真偽値判定用述語です。そしてSchemeに於いては空リストは#fではない。要するにANSI Common LispとSchemeではブーリアンの体系が違うんですよね。Schemeはいわゆる普通のプログラミング言語と同様に、ブーリアン用の体系があるわけですけど、ANSI Common Lispはちょっとしたハックに見える。


従って、ANSI Common Lispでは次のような一見わけの分からない事が可能です。



CL-USER> (cons 1 (> 2 3))
(1)
CL-USER>

(> 2 3)は偽なんでnilを返すわけなんですけど、それはすなわち空リストです。そこに1をconsして、リストにしちゃう……。「こんなんで大丈夫なのか?」とか思うでしょうが、実はこのnilの性質は色んな局面で結構役に立ちます(※)。プログラムが短くなったりするんですよ。



※: ちなみに、僕が最初に触ったのはANSI Common Lispの方で、「随分大雑把な設計だな」とか感じました。が、慣れると病みつきになって、最初Schemeを触った時は#t#fの存在がどうにも馴染めませんでした。っつーか、ハッキリ言うと嫌いだった。


例えば、The Little Schemerに次のようなお題があるらしいんですけど。




;;; The Little Schemer の member? の回答例
> (define (member? a lat)
(and (pair? lat) ;いちいちlatがどうなのか検査しなければならない。
(or (eq? a (car lat))
(member? a (cdr lat)))))
> (member? 'a '(f u c k o f f!))
#f
>


ANSI Common Lispだったら次のように書けます。



CL-USER> (defun member? (a lat)
(and lat ;これだけで充分
(or (eq a (car lat))
(member? a (cdr lat)))))
MEMBER?
CL-USER> (member? 'a '(s o n o f a b i t c h !))
T
CL-USER> (member? 'a '(f u c k o f f !))
NIL
CL-USER>


リスト再帰は構造上、ベースケースとして空リストが停止条件になる場合が多いんですが、空リスト=nilと定義しているCommon Lispではこの性質が大活躍します。上の例だと、andnilを見つけると即刻評価を中止してnilを返すのが肝なんですけど、latはリストである事が前提で、かつ、そこに対して特殊な判定は必要ないわけです。latが空リストになった途端、andはそれを偽値と解釈して再帰は滞り無く終わる。



上の例は単純ですが、色んな場面で効いてくる、Common Lispの便利な側面です。




偽と空リストに対して同じものを使うと混乱を引き起こすことがときどきあるが、長年のLispプログラミングの中で私はそれが掛け値なしの勝利であることを確信している。なぜなら、空リストは集合論での偽であり、多くのLispプログラムは集合で考えるからである。




ANSI Common Lispのcarcdr


さて、偽値=空リストであるANSI Common Lispのnilなんですが。Schemeと違って次の大変美しい性質があります。それは空リストにcarcdrを適用したらどうなるか、と言う話です。


Schemeの場合、空リストにcarcdrを適用するとエラーを返してきます。



> (car '())
car: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

> (cdr '())
cdr: expects argument of type ; given ()

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>

一方、ANSI Common Lispではそう言う事がありません。



CL-USER> (car '())
NIL
CL-USER> (cdr '())
NIL
CL-USER>


nilcar/cdrを適用するとnilが返る。これは大変ありがたい仕様です。



これは実はInterlisp由来の仕様だそうなんですが、いずれにせよ大変便利で、それがCommon Lispの仕様として取り入れられたそうです。



ANSI Common LispとSchemeのcar/cdrの違いは、そのままそれらを利用した組み込み関数/手続きの挙動にも影響しています。代表的なのは次の機能でしょうか。
















Scheme ANSI Common Lisp
list-ref nth
list-tail nthcdr


両者とも引数の順序が違うんですが、大体同じ機能です。ただし、nilcar/cdrの扱いの違いで、挙動に差が生じるのです。


例えば、LOLで紹介されている(正確に言うとOn Lispで紹介されている)groupと言う関数があります。コードは以下の通りです。



(defun group (source n)
(if (zerop n) (error "zero length"))
(labels ((rec (source acc)
(let ((rest (nthcdr n source)))
(if (consp rest)
(rec rest (cons (subseq source 0 n) acc))
(nreverse (cons source acc))))))
(if source (rec source nil) nil)))

Schemeでそのまま何も考えないで直訳すると次のような感じでしょうか。




;; Gauche なら (use srfi-1)
;; Guile なら (use-modules (srfi srfi-1))
(require srfi/1) ;PLT Scheme 依存

(define (group source n)
(and (zero? n) (error "zero length"))
(letrec ((rec (lambda (source acc)
(let ((rest (list-tail source n))) ;list-tail の引数の順番に注意!
(if (pair? rest)
(rec rest (cons (take source n) acc)) ;take が srfi-1 の手続き
(reverse (cons source acc)))))))
(and (pair? source) (rec source '()))))


ところがどっこい、これは動きません。




> (group '(a b c d e f g) 2)
list-tail: index 2 too large for list: (g)

=== context ===
stdin::7328: group
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>


list-refが「sourceリストの長さが足りない!」と文句を言ってくる。つまり、こう言う事です。












ANSI Common Lispのnth-cdrの挙動 Schemeのlist-tailの挙動


CL-USER> (nthcdr 2 '(g))
NIL
CL-USER>



> (list-ref '(g) 2)
list-ref: index 2 too large for list: (g)

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

>



groupは与えられたsourceリストに対して、再帰的に要素をn個ずつグルーピングしていくわけですが、結局ベースケースで問題が生じてくるわけです。ANSI Common Lispでは考えなくて済むんですが、Schemeの場合、「現時点でのリストがどう言う状況なのか?」一々チェックを入れないとなりません。この場合は常にsourceの長さがnより大きいかどうか見なければならない。


従って、Schemeだとここまで面倒みないとなりません。




> (define (group source n)
(and (zero? n) (error "zero length"))
(letrec ((rec (lambda (source acc)
(let ((rest (if (< (length source) n) ;リスト source の長さを n と比較する
'() ;空リストを返す
(list-tail source n))))
(if (pair? rest)
(rec rest (cons (take source n) acc))
(reverse (cons source acc)))))))
(and (pair? source) (rec source '()))))
> (list-ref '(g) 2)
> (group '(a b c d e f g) 2)
((a b) (c d) (e f) (g))
>


CLerだったら


「やってらんねえよ!!!」

とか言うんでしょうね(笑)。人間一旦ラクを覚えたら引き返せませんので(笑)。



いずれにせよ、ANSI Common Lispではnilに対するcar/cdrがエラーにならないお陰で、書くべきプログラムが短く済む事が多いのです。



ANSI Common Lispにはラムダ・リスト・キーワードがいっぱい



Schemeの手続きに於いては必須パラメータとレスト・パラメータくらいしかありません。まあ、実装によるんですが、仕様(R5RS)ではそうなっていますね。


一方、ANSI Common Lispでは必須パラメータ「その他」がたくさんあります。必須パラメータ「以外」をラムダ・リスト・キーワード等と呼ぶようです。


#9LISP の方で@aharisuさんが紹介していたんですが、ここでもザックリまとめておきたいと思います。





レスト・パラメータはSchemeのヤツと同じなんで、ここでは端折ります。キーワード・パラメータに付いては後回しにします。まずはオプショナル・パラメータを見てみます。



例えば、Schemeを扱っていて、末尾再帰で手続きを書くのに便利な構文にnamed-letなんてのがあります。ワンパターンで階乗手続きを定義しますが、次のように使いますね。



> (define (fact n)
(let loop ((n n) (acc 1))
(if (zero? n)
acc
(loop (- n 1) (* n acc)))))
> (fact 10)
3628800
>

実際これは便利で、LOLも本格的なマクロ実装の最初の例として、このnamed-letをまずは実装しよう、と言う流れになっています。


どうしてこんなに楽に末尾再帰が書ける構文をCommon Lispは持ってないんだろう?平たく言うとその必要が無いからって事でしょうね(※)。ANSI Common Lispにはオプショナル・パラメータがある。




オプショナル・パラメータとは文字通りオプショナルなパラメータで、関数呼び出しのときに対応する引数があってもなくてもよい。

~(中略)~

オプショナル・パラメータは、もし対応する引数があればその引数を初期値とし、そうでなければ、ラムダ・リストに指定された値(デフォルト値という)を初期値とする。個々のオプショナル・パラメータは、一般に三つの要素からなるリストで指定する。


    (<<変数>> <<式>> <<判定変数>>)

~(中略)~

判定変数が必要なければ、オプショナル・パラメータ指定の<<判定変数>>のところを省略して

    (<<変数>> <<式>>)

だけでもよい。また、デフォルト値を特に指定する必要がなければ

    (<<変数>>)

だけでもよい。これらの場合はデフォルト値としてnilが使われる。




つまり、オプショナル・パラメータに適当な初期値を与えれば、named-let無しでも簡単に末尾再帰が書けて、結果Scheme版より短くコードを記述する事が可能となるわけです。



CL-USER> (defun fact (n &optional (acc 1)) ;オプショナル・パラメータとして acc を初期値1として設定
(if (zerop n)
acc ;返り値は acc
(fact (1- n) (* n acc)))) ;オプショナル・パラメータ acc を操作する
FACT
CL-USER> (fact 10) ;計算はオプショナル・パラメータ無しで
3628800
CL-USER>


もう一つ例を挙げましょう。これはオプショナル・パラメータがどうの、と言うよりLisp-2であるための説明なんですが、ここで見てみます。


The Little Schemerrember-fと言う問題があるそうで、これは高階手続きの問題のようですが、Schemeのnamed-letを用いると次のように定義出来ます。



> (define (rember-f test? a l)
(let loop ((l l) (acc '()))
(if (null? l)
(reverse acc)
(loop (cdr l) (let ((it (car l)))
(if (test? a it)
acc
(cons it acc)))))))
> (rember-f (lambda (x y)
(= x y)) 5 '(1 2 3 4 5))
(1 2 3 4)
> (rember-f (lambda (x y)
(eq? x y)) 'jelly '(jelly beans are good))
(beans are good)
> (rember-f (lambda (x y)
(equal? x y)) '(pop corn) '(lemonade (pop corn) and (cake)))
(lemonade and (cake))
>


これもANSI Common Lispでは、上で見た通り、オプショナル・パラメータを用いた末尾再帰で基本的には実装可能なんですが、実際やってみると、何故かエラーを返してきます。




CL-USER>> (defun rember-f (test? a l &optional (acc nil))
(if (null l)
(reverse acc)
(rember-f test? a (cdr l)
(let ((it (car l)))
(if (test? a it)
acc
(cons it acc))))))

; (TEST? A IT)
;
; caught STYLE-WARNING:
; undefined function: TEST?
;
; compilation unit finished
; Undefined function:
; TEST?
; caught 1 STYLE-WARNING condition
REMBER-F
CL-USER>


上はSBCLでの警告表示ですが、(test? a it)test?なんて関数はないぞ、と文句を言ってくる。Schemeだと何ともないんですが、Common Lispじゃダメだ、って事ですね


何が悪いのか、と言うと、仮引数はデフォルトでは変数が来る、と解釈されているわけです。ところがtest?と言う仮引数は関数のポジションであるS式の第一要素に鎮座してる。Common Lispは「それじゃマズいだろ」って言ってるわけです。


これを解決するのが、Schemerの中で悪名高いCommon Lispのfuncallです。つまり、こう書けば良い。




CL-USER> (defun rember-f (test? a l &optional (acc nil))
(if (null l)
(reverse acc)
(rember-f test? a (cdr l)
(let ((it (car l)))
(if (funcall test? a it) ;ここで使う
acc
(cons it acc))))))
REMBER-F
CL-USER> (rember-f #'(lambda (x y)
(= x y)) 5 '(1 2 3 4 5))
(1 2 3 4)
CL-USER> (rember-f #'(lambda (x y)
(eq x y)) 'jelly '(jelly beans are good))
(BEANS ARE GOOD)
CL-USER> (rember-f #'(lambda (x y)
(equal x y)) '(pop corn) '(lemonade (pop corn) and (cake)))
(LEMONADE AND (CAKE))
CL-USER>


最後に補助変数&auxです。これはあんまり使われてるの見たことないんで、Common Lisp入門から解説を引っ張ってきます。




補助変数とは関数内で局所的に使用する変数のことで、関数の引数とは関係ない。局所変数は、letlet*などのスペシャル・フォームを使っても宣言できるが、ラムダ・リスト・キーワード&auxを使うことによって、関数定義がコンパクトになる。例えば

    (lambda (a b) (let* ((c 1) (d 2)) ...))

と書くかわりに&auxを使って、

    (lambda (a b &aux (c 1) (d 2)) ...)

と書くことができる。各補助変数指定は、変数名とその初期値を与える式からなるリストである。

    (<<変数>> <<式>>)

<<式>>がnilの場合はこれを

    (<<変数>>)

あるいは単に

    <<変数>>

と略してもよい。


だそうです。


確かに便利そうですが、あんま使われてるのを見ないのは、ひょっとしたらCLerはletの方が好きなのかもしれませんね。




※: もっとも、LOLの流れから言うと、「もっとも簡単なマクロで作る制御構文」の例としてnamed-letを敢えて取り上げたと思われる。

もう一つの狙いが「マクロに最適化したコードを吐かせる」例として、魅力的な題材だったのだろう。実際、ANSI Common Lispの仕様はSchemeと違って末尾再帰の最適化を要求していない。

ただし、「末尾再帰の最適化をしない」Common Lispの実装を思いつく方が難しい、と言うのも事実である。ちなみに、GNU CLISPの場合、インタプリタ上で入力しただけの末尾再帰関数に関しては最適化しないが、バイトコードへコンパイルすると最適化を行う模様である。


ANSI Common Lispの組み込み関数は汎用/統合指向



Schemeにはmemqmemvmemberと言う組み込み手続きがあります。それぞれ二つ引数を取り、第二引数はリストで、第一要素と等価なcarを持つ第二引数の部分リストを返します。ただし、この「等価」の定義が違って、memqeq?memveqv?memberequal?を比較に用います。




> (memq 'a '(a b c))
(a b c)
> (memq 'b '(a b c))
(b c)
> (memq 'a '(b c d))
#f
> (memq (list 'a) '(b (a) c))
#f
> (member (list 'a)
'(b (a) c))
((a) c)
> (memq 101 '(100 101 102)) ;ここの動作は未定義
(101 102) ;実装によって結果が異なる
> (memv 101 '(100 101 102))
(101 102)
>


一方、ANSI Common Lispにはmemberしかありません。ただし、ANSI Common Lispのmemberは高階関数として設計されていて、比較演算子をキーワード・パラメータとして受け取る事が出来ます。




CL-USER> (member 'a '(a b c) :test #'eq) ; :test がキーワードパラメータで、ここで比較関数を指定
(A B C)
CL-USER> (member 'b '(a b c) :test #'eq)
(B C)
CL-USER> (member 'a '(b c d) :test #'eq)
NIL
CL-USER> (member (list 'a) '(b (a) c) :test #'eq)
NIL
CL-USER> (member (list 'a)
'(b (a) c) :test #'equal)
((A) C)
CL-USER> (member 101 '(100 101 102) :test #'eq) ;ここは実装依存の結果が返る
(101 102)
CL-USER> (member 101 '(100 101 102)) ;デフォルトは eql による比較
(101 102)
CL-USER>


それどころか、CLHSに記載されている通り、比較対象の要素まで指定する事が出来ます。




;; ドット対のcdrを見て等価じゃないところから返せ、と言う指定
CL-USER> (member 2 '((1 . 2) (3 . 4)) :test-not #'= :key #'cdr)
((3 . 4))
CL-USER>


このように、ANSI Common Lispの一部の関数は、Schemeではバラバラになっている手続きを統合したような単一関数として設計されているパターンが多いです(※)。もはや「組み込み関数」と言うより立派な一つのプログラムである、と言って良いくらいでしょう。非常に大掛かりなのがANSI Common Lispの関数の特徴です。



もう一つこの手の代表的なものに、連想リストへのアクセサ、assocが挙げられます。Schemeではassqassvassocが用意されています。




> (define e '((a 1) (b 2) (c 3)))
> (assq 'a e)
(a 1)
> (assq 'b e)
(b 2)
> (assq 'd e)
#f
> (assq (list 'a) '(((a)) ((b)) ((c))))
#f
> (assoc (list 'a) '(((a)) ((b)) ((c))))
((a))
> (assq 5 '((2 3) (5 7) (11 13))) ;これは返り値は未定義
(5 7) ;実装依存の結果が返る
> (assv 5 '((2 3) (5 7) (11 13)))
(5 7)
>


当然ANSI Common Lispではassocは高階関数として設計されていて、比較関数はキーワード・パラメータで変更するようになっています。




CL-USER> (defparameter e '((a 1) (b 2) (c 3)))
E
CL-USER> (assoc 'a e :test #'eq)
(A 1)
CL-USER> (assoc 'b e :test #'eq)
(B 2)
CL-USER> (assoc 'd e :test #'eq)
NIL
CL-USER> (assoc (list 'a) '(((a)) ((b)) ((c))) :test #'eq)
NIL
CL-USER> (assoc (list 'a) '(((a)) ((b)) ((c))) :test #'equal)
((A))
CL-USER> (assoc 5 '((2 3) (5 7) (11 13)) :test #'eq) ;返り値は実装依存
(5 7)
CL-USER> (assoc 5 '((2 3) (5 7) (11 13))) ;デフォルトは eql による比較
(5 7)
CL-USER>



※: 最初のSchemeは1975年に登場、ANSI以前のCommon Lispは1984年登場なんで、約10年の開きがあるわけです。仕様自体はCommon Lispの方が新しい。

調べてみた限り、Schemeのmemq/memv/memberassq/assv/assocは共にMITのMacLisp由来で、Schemeでの名称もこれを受けています。一方、約10年後に登場したCommon Lispではキーワード・パラメータの導入と共に、積極的に統合化へ進んだ、と見た方が良いでしょう。

なお、MacLisp直系の子孫であるEmacs Lispでは、やはりSchemeのようにmemq/memv/memberassq/assv/assocと言う形になっている模様です。


Schemeにはいくつもの「データ変換用」の手続きが用意されていて、それらは大体シンボルに->と言う記号が入っています。中でも良く使うのがリスト、ベクタ、文字列等の通称シーケンスと呼ばれるデータ型同士の変換手続きです。



  • list->string

  • list->vector

  • string->list

  • vector->list




> (list->string '(#\爆 #\発 #\し #\ろ #\!))
"爆発しろ!"
> (list->vector '(0 1 2 3 4 5 6 7 8 9))
#(0 1 2 3 4 5 6 7 8 9)
> (string->list "爆発した!")
(#\爆 #\発 #\し #\た #\!)
> (vector->list #(0 1 2 3 4 5 6 7 8 9))
(0 1 2 3 4 5 6 7 8 9)
>


これらは当然便利なんです。が、CLHSでそれらに対応する関数を探そうとしてもなかなか見つからない。まあ、見つからないのは当然で、ANSI Common Lispではこれらを統一的に扱う高階関数coerceに置き換わっています(しかも何て読むんだか分からない・笑)。




CL-USER> (coerce '(#\爆 #\発 #\し #\ろ #\!) 'string)
"爆発しろ!"
CL-USER> (coerce '(0 1 2 3 4 5 6 7 8 9) 'vector)
#(0 1 2 3 4 5 6 7 8 9)
CL-USER> (coerce "爆発した!" 'list) ;ユニコード指定文字のリストに変換される
(#\U7206 #\U767A #\HIRAGANA_LETTER_SI #\HIRAGANA_LETTER_TA #\!)
CL-USER> (coerce #(0 1 2 3 4 5 6 7 8 9) 'list)
(0 1 2 3 4 5 6 7 8 9)
CL-USER>


ちなみに、その他の変換手続きへの対応表は以下の通りとなっています。








































Scheme ANSI Common Lisp
char->integer char-code
exact->inexact float
inexact->exact rational
integer->char code-char
number->string write-to-string
string->number parse-integer
string->symbol intern
symbol->string symbol-name/string


ANSI Common Lispにはfoldがない



けどreduceはあります。@valvallowさん大喜び(謎)。




;;; http://valvallow.blogspot.com/2010/04/lol-flatten.html
;;; を参照
;;; 形式的には全く同じコードだと分かる
;;; CL の reduce は SRFI-1 のfold/fold-right/reduce/reduce-right
;;; をキーワード・パラメータを用いて統合化したようなもの

CL-USER> (defun flatten (tree)
(reduce #'(lambda (e acc)
(cond
((null e) acc)
((consp e)
(append (flatten e) acc))
(t (cons e acc))))
tree :from-end t :initial-value nil)) ;初期値に nil を指定して tree を逆順に辿る
FLATTEN
CL-USER> (flatten '(1 2 3 (4 5 6 (7) ((8) 9)) ((() 10))))
(1 2 3 4 5 6 7 8 9 10)
CL-USER>


ANSI Common Lispのsetfは超強力



Schemeのset!にあたるANSI Common Lispの推奨マクロがsetfです。setf汎用構造修正子としてデザインされています。


両者の使い方は基本的には同じです。











Scheme ANSI Common Lisp


> (define a 1) ;define の返り値は未定義
> (set! a '(x y z)) ;set! の返り値も未定義
> a
(x y z)
>


CL-USER> (defparameter a 1) ;defparameter の返り値は束縛されたシンボル
A
CL-USER> (setf a '(x y z)) ;setf の返り値は束縛されたデータ
(X Y Z)
CL-USER> a
(X Y Z)
CL-USER>


ただし、まずsetfの特徴として、複数の変数を逐次、破壊的に変更する事が可能です(※1)。これがSchemeのset!には出来ない。












Scheme ANSI Common Lisp


> (define a 1)
> (define b 2)
> (define c 3)
;;; 複数の変数を一気に変更しようとすると、当然エラーが出る
> (set! a 'x b 'y c 'z)
stdin::10037: set!: bad syntax (has 6 parts after keyword) in: (set! a (quote x) b (quote y) c (quote z))

=== context ===
/usr/lib/plt/collects/scheme/private/misc.ss:74:7

> (set! a 'x)
> (set! b 'y)
> (set! c 'z)
> a
x
> b
y
> c
z
>


CL-USER> (defparameter a 1)
A
CL-USER> (defparameter b 2)
B
CL-USER> (defparameter c 3)
C
;;; 複数の変数を一気に変更する事が出来る
CL-USER> (setf a 'x
b 'y
c 'z)
Z ;返り値は最後に束縛されたデータ
CL-USER> a
X
CL-USER> b
Y
CL-USER> c
Z
CL-USER>


また、setfの第一引数は式でも良く、第一引数で参照された場所の値を破壊的に変更する事が可能で、これもSchemeのset!には出来ない事です(※2)。




CL-USER> (defparameter x '(a b c))
X
CL-USER> (setf (car x) 'n)
N
CL-USER> x
(N B C)
CL-USER>



※1: しかしながら「出来る」と言うのと「人気がある」と言うのは別っぽい。Common Lispにはもうちょっとプリミティヴな特殊形式であるsetqがあり、これも逐次で変数の値を破壊的に書き換える機能がある。同じ特殊形式がEmacs Lispにも存在するが、色んなelispファイルを眺めても、この「複数の値を一気に書き換える」書き方はあまり成されてないようである。それよりもsetqを羅列する方が好まれてる模様だ。

※2: SRFI 17でANSI Common Lispのsetfと同等の機能を持つ一般化set!が提案されてはいる。が、動かない処理系が出てきてるので要注意。

一般化set!をマクロで実装するにはset-car!set-cdr!と言う手続きが必要なのだが、これらはR6RS辺りからさほど重要な手続きではなくなってきた模様である。

    set-car!, set-cdr!: ペアの要素を変更可能にすると 特に実装者にとって色々面倒なことが起きるので、R6RS案の段階ではペアは 全て変更不可にしようぜ、みたいな極端な話が出てきたこともありました。 結局、これらの手続きを別ライブラリにすることでなんとなく妥協。 ペアを変更不可にすると、循環リストが出来ないんで色々見通しが良くなるんですね。 個人的には破壊的変更が嫌ならHaskell使えばって思いますが。



これを受けて、PLT SchemeがVersion: 4.0.0をもってset-car!/set-cdr!を廃止(Getting rid of set-car! and set-cdr!)。他にもScheme48のような海外でメジャーな処理系もset-car!/set-cdr!を備えていない。

Schemeの仕様に準じてないのにSchemeを名乗るのはおかしいだろ、って話もあるが、PLTは既に「Scheme方言である」と明言し、また、そもそもANSIと違って仕様書の縛りがそれほどキツくない。実装者側にとってのSchemeの魅力とは、言語仕様が比較的小さく、コアをラクに実装出来(とは言ってもそれはそれで難しい)、かつ自分のオリジナルのアイディアを組み込みやすいことである。従って処理系間の互換性が落ちやすい、と言うのがどの道前提になる(これはANSI Scheme仕様書であるR4RS準拠実装を今全くと言っていいほど見かけない、って事からも分かる)。

現時点ではR6RS準拠を謳っている実装はPLTくらいしかないが、今後Schemeはどうなっていくか分からない。いずれにせよ、当分の間set-car!/set-cdr!はアテにせん方が良いような気がする。


ANSI Common Lispはcase-insensitive



R5RSには明言されてないと思うんですけど、現存する実装を見る限り、Schemeはcase-sensitive(シンボルの大文字/小文字を区別する)言語です。R6RSでは、ハッキリとcase-sensitiveな言語となった模様です。




Lispの長年の伝統をついに破って、識別子がcase-sensitiveになりました。 もっともこれは現状追認とも言えます (参考:x:Concept:CaseSensitivity)。

処理系はオプショナルなcase-insensitive modeを用意しても 良いことになっています (6app:B)。


一方、ANSI Common Lispの方は、既に気づいたと思いますが、Lispの伝統に則り、case-insensitive(シンボルの大文字/小文字を区別しない)言語です。従って、両者のシンボルの等価判定には差が出てきます。












Schemeの場合 ANSI Common Lispの場合


> (eq? 'hoge 'HOGE)
#f
>


CL-USER> (eq 'hoge 'HOGE)
T
CL-USER>


ANSI Common LispはC登場以前の言語群の伝統を継承しているんですね。シンボル表記に関して言うと、ANSI Common LispはFortran/BASIC/Pascalの仲間です(っつーかUNIX文化じゃない)。



ANSI Common Lispには継続も遅延評価も無い



そんなつや~なものはANSI Common Lispにはありません(笑)。元々、Scheme自体が研究用途だった、と言う事もあって、実験的/先鋭的機能(※)を入れてるんですが、ANSI Common Lispはもっと堅実な仕様になっています。「より普通の」プログラミング言語なんですね。




※: とは言っても、R6RSの本体から遅延評価は消えた模様です。


    delay, force: R6RS本体では特に遅延評価のためのプリミティブは 提供されません。というのは、遅延評価メカニズム自体はSchemeの手続きの上に簡単に 構築できるのと、R5RSのdelayとforceはunderspecifiedで、実用に供するためには 結局再実装が必要であった (srfi-40およびsrfi-45をめぐる議論参照。x:SRFI-40, x:SRFI-45)、という経験によります。




ANSI Common Lispでは、大域脱出をする際には通常、特殊オペレータreturn-fromか、あるいはreturnマクロを用います。



例えば、次のような問題を考えます。




引数のリストの要素が全てpred?を満たすか判定するlist-of-pred?手続き/関数を定義してみる











Scheme: call/cc での解 ANSI Common Lisp: return-from での解

(define (list-of-pred? lst pred?)
(call/cc
(lambda (return)
(and (map (lambda (x)
(let ((it (pred? x)))
(if it it (return it)))) lst)
#t))))

(defun list-of-pred? (lst pred?)
(and (mapcar #'(lambda (x)
(let ((it (funcall pred? x)))
;; return-from は第一引数に関数名を取り、
;; そこから第二引数の値を持って大域脱出する
;; 第二引数が省略された場合のデフォルト値は nil
(if it it (return-from list-of-pred?)))) lst)
t))


なお、On LispではCommon Lispでの遅延評価の実装例継続の実装例が紹介されています。



ANSI Common Lispは(過剰に)親切設計



ANSI Common Lispは非常に巨大な仕様で有名なんですが、何と仕様にデバッグ関連のツールまで含まれているんです。デバッグ関連ツールまで(ある程度にせよ)定義されている言語なんて殆ど無いでしょう。一方、Schemeはそう言う部分は丸っきり無くって、まるでCSの宿題を解く為の言語の如しです。自分で解いて考えてくれ、と言わんばかり。



有名なのはtraceマクロでしょう(※1)。例えば再帰関数を定義して、何かおかしな結果が返るような場合、関数の挙動を逐一追ってくれたりします。




CL-USER> (defun fact (n &optional (acc 1))
(if (zerop n)
acc
(fact (1- n) (* n acc))))
FACT
CL-USER> (trace fact)
(FACT)
CL-USER> (fact 3)
0: (FACT 3)
1: (FACT 2 3)
2: (FACT 1 6)
3: (FACT 0 6)
3: FACT returned 6
2: FACT returned 6
1: FACT returned 6
0: FACT returned 6
6
CL-USER>


なお、traceを解除するにはuntraceを使います。



ちなみに、残念ながらtraceは局所関数の中身まで追っかけてきてくれません。これがSchemerとCLerのアティテュードの差をある程度生んでいて、Schemerはトップレベルでのシンボルの衝突を避ける意味もあって、ローカル手続きを多用する傾向があるんですが、CLerはSchemer程局所関数を多用せず、どちらかと言うと大域関数として補助関数を複数作る事を好みます。CLerはトップレベルのシンボルの衝突はいざとなったらパッケージで回避出来るので、デバックのやりやすさをより重要視する傾向があるんです。



また、ANSI Common Lispでは、エラーが起こるとバックトレースあるいはブレイクループと呼ばれる状態に入ります。ANSI Common Lispではこれがデバッガの基本となっていて、関数が止まった地点から逆順に何が起こったか探索していけるように設計されています。



例えばC言語を勉強したとしてもデバッギングはまた別です。「GDBの使い方」なんかを学ばないとならない。ブレークポイント設置もメンド臭いし、何よりこれは「言語の外」の話です。言語に精通したとせよ、プログラミングするには「その他のソフトウェアの使い方」を強要される。じゃないとマトモにプログラミングが出来ない。



一方、ANSI Common Lispは言語仕様の中にデバッガがある。ANSI Common Lispは一種オールインワンの言語環境を目指していて、そこに何でもあるわけです。それ以外に何も必要がない、最初からデバッガの使い方も簡単に学べる、と言う辺りがCLerの「CL最強説」の一つの源になっているのでしょう(※2)。




※1: もちろんSchemeにも実装依存でtraceを持ってる処理系があります。ただし、ここでは「実装依存がどーの」とか言うつもりは全くありません。やはりプログラミングのしやすさを考えると仕様で定義されてるべきじゃないか、と思います。逆に言うと、定義してないのは、やっぱりSchemeは宿題の為の言語なのでは、と。

なお、Gaucheで使えるtraceSLIBのものです。SRFIとはまた別の共有ライブラリなんですが、一方、PLT SchemeではSLIBは使用出来ません。

PLT Schemeでは(require mzlib/trace)を評価した後、CLと全く同じようにtraceが使えます。

Gaucheでは

(use slib)
(require 'trace)

traceが使用出来ます。

Guileではそのままデフォルトでtraceが使用可能になっています。

※2: この「ANSI Common Lispは言語環境の統合化を狙っている」ってのはマジな話で、例えばedなんて関数が仕様で定義されていて、これを走らせると「エディタを立ち上げる」事が要請されています。

Windows上のCLISPで試した事があるんですが、何と「メモ帳」が立ち上がって大爆笑した事があります(笑)。ANSI仕様では何とテキストエディタまで内包されているんです(もっとも仕様に実装依存で構わんと明記されてるんですが)。

この辺、IDEの機能であるべきものを言語仕様に含めるべきではない、と言う意見もあるとは思います。しかし、ANSI Common Lispはそれをやっちゃった。

CLerの選民思想に関する批判があって、それはその通りだと思います。ただ、敢えて言うと、「オールインワンの言語環境を提供する」ってのは別の見方をすると「ラクだ」って事でもあるんですよね。誰に対してラクか、と言われれば当然エキスパートにとって、って事もあるんですけど、プログラミング初心者にとってもラクだと言う事でもあるんです。

CLは「言語設計者にとって良い事はプログラマにとっても良い事だ」が設計思想だ、と言われてますが、同時に「プログラミング初心者にとってもラクな事はエキスパートにとってもラクだ」と言う設計思想もあるような気がします。本当はこの二つは境界線があるべきものじゃない。そしてBASICみたいにやたら初心者におもねってるわけでもないのです。

「CLerの苛立ち」と言うのは選民思想と言うよりは、

    「こんなにラクな環境を構築してるのに何で人気が無いんだろ?」

って部分に根ざしているような気がします。エラーが出たら自動的にデバッガが立ち上がったりする、ってCLの設計を見ても、どっちかと言うと、

    どんなアンポンタンでもプログラムがデッチ上げられる

ように設計されているようにしか見えない、のです。その親切設計に気づかないで

    「括弧が多いから・・・・・・。」

と文句言われたらそりゃあ頭にも来るし、引きこもるでしょう(笑)。そう言う部分はある、のです。括弧は大した問題じゃない傍流の話だから、です。

ANSI Common Lispはプログラミングのビギナーからエキスパートまでの幅広い層を対象に設計されてます。これは事実です。すべての人々のレベルに合わせて応えてくれる。

残念ながら、Common Lispの内側に住む限り最強でしょうが、90年代初頭と違ってCLの外界はどんどん様変わりしています。OSとやり取りするのが一番難しい、と言うのが現状でしょう。結局、ライブラリの問題に帰着するって現象が起きている。

Lispで書かれたOSでCommon Lispを動かす、のが実際は最強なんでしょうが、現時点では、過去のLisp OSのエミュレータか、あるいはハードウェアエミュレータくらいしか無いのが残念です。


その他細々した違い




  • Schemeの手続き/特殊形式名とANSI Common Lispの関数/特殊オペレータ名が食い違っている


    今までもちょこちょこと出てきましたが、結構名前が違うものが多いです。代表的なところで、SchemeのbeginはANSI Common Lispではprognとなっています。

  • Schemeの仕様では述語の最後は?で終わる命名規約になっている


    一方、ANSI Common Lispでは慣習的には述語はpで終わる、とされていますが、仕様内で定義されている述語を見ると、必ずしもそうじゃありません(例:atomnull)。また、Schemeでは破壊的変更を行う手続きに関しては!で終わる命名規約になっていますが、その手の命名規約はCommon Lispにはありません。これらのゴチャゴチャは、Common Lispの元となったZetalisp、MacLisp、Interlispで書かれたコードと最大限互換性を保つ為、だと思われます。要するに結構デタラメです。色んな意味で(笑)。

    なお、独自に述語を作る際、実践Common LispやLOLではCommon Lisp流の-pスタイルを使っていますが、ポール・グレアムはScheme式に?で終わらせる事を好んでる模様です。

  • ANSI Common Lispでは(if pred? then)で述語部が偽の場合、nilを返す


    Schemeではこの場合、返り値が未定義で、処理系によってはこの書き方は単にエラーになります。一方、ANSI Common Lispではどんな場合でも値を返すので素直にnilを返してくれます。

  • Schemeのcondの「その他」はelseだが、ANSI Common Lispでは単にt


    これはもうまんまそのまんま、です。

  • Schemeのcaseの「その他」はelseだが、ANSI Common Lispでは何故かotherwise


    何ででしょ?もう知らん(笑)。



ANSI Common Lispのマクロ



衛生的マクロから伝統的マクロへ




Schemeが方向を誤ったのは、マクロの構築を目的とするドメイン固有言語を推進したことである。Schemeのミニ言語は確かに強力だが、マクロの勘所を完全に外している。マクロは初心者用プリプロセッサ言語ではなく、Lispで書くから偉大なのだ。



Schemeの衛生的マクロ、特にR5RSのヤツは、パターンマッチングと言う武器を使って、分かりやすいテンプレート変換を用いた機能の提供を行っています。確かに比較的分かりやすく、敷居は低いんです。


ただし、これがLispなのか?と言うと……実際問題まるで別物です。LOLで示唆されているように、Scheme自体の文法とまるで関係なく衛生的マクロは存在している。つまり、仕様としてはSchemeはScheme+別のミニ言語を提供しているんだ、って言って良いでしょう。


とは言っても、Schemeの衛生的マクロを見た後だと、比較的ラクにANSI Common Lispのマクロの世界には入っていきやすいとは思います。何故ならどの道狙いは同じだから、です。目的はパターンの変換。そして、単にANSI Common Lispのマクロは衛生的マクロより強力だ、ってだけの話です。



例えば、letを実装しろ、って課題があったとします。教科書的にはletlambdaの構文糖衣なんで、ANSI Common Lispのマクロで単純に書けば次のようになるでしょう。




(defmacro my-let (bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


ちょっといきなりなんで見にくいとは思いますが、敢えてインデントを揃えてSchemeの衛生的マクロでの解と照らし合わせてみれば、次のように対応している事が分かるでしょう。












Schemeの衛生的マクロでの解 ANSI Common Lispのマクロでの解


(define-syntax my-let
(syntax-rules ()
((_ ((x v) ...) body ...)
((lambda (x ...)
body ...)
v ...))))

(defmacro my-let

(bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


比べてみると、


  1. Schemeの衛生的マクロの方が(syntax-rules () ...)の為に記述要素が多い。

  2. Schemeの方が明示的なパターン変換を指定する為、括弧が多い。

  3. ANSI Common Lispのdefmacroのパラメータ・リストが衛生的マクロによる記述パターンに対応している。

  4. 他はこのレベルでは構成自体は大して変わらない。




と言う事が見て取れます。概形自体は全く同じなんです。


「ANSI Common Lisp版には`とか,@とかワケの分からん記号が跋扈してるぞ…!」

と言う感想はどーでもいいです(笑)。まずはザックリと「構成自体は似てる」って感覚をまず掴む事が大事だと思います。



衛生的マクロの(syntax-rules () ...)に関して言うと、僕もこれが何の為にあるんだか、ぶっちゃけ分からないです(笑)。これしかないんだったら、明示せんでもエエんちゃうの?とか思ってるんですが(実際は、R6RSにはsyntax-caseと言うものもあり)。まあ、恐らく、Schemeのλ式による手続き定義と対応させるためだけに入ってるんでしょうけどね。












Schemeの手続き定義の形式 Schemeのマクロ定義の形式

(define 手続き名
(lambda (引数)
...))

(define-syntax マクロ名
(syntax-rules (キーワード)
...))


形式的な一貫性を保つ為なのかどうか知りませんが、それだけの為の整合性ってのは正直イラつきますね(笑)。タイプ量が増えるだけ、なんで。実際、実装によっては、define-syntax(syntax-rules () ...)を独立で提供しつつ、なおかつこの二つを統合したマクロを提供している処理系もあります(例:PLT Schemeのdefine-syntax-rule)。反面、ANSI Common Lispのマクロにはそう言うイラつき要素はありません。



いずれにせよ、最初はSchemeの衛生的マクロで鍛えた「パターン変換」を念頭に入れてマクロを記述するようにした方が早く慣れるんじゃないか、とは思います。



以下は古いLispの本の筋書きに則った展開です。



クオートとlistは違う



何を当たり前な事を、と言う話なんですけど、実は手癖で忘れる可能性が高い話なんじゃないか、と思います。



例えば、(1 2 3 4 5)と言うリスト*lst*を定義せよ、と言われたら殆どの人が




(defparameter *lst* '(1 2 3 4 5))


と書くでしょう。まあ、十中八九書きますよね。でもリストを生成する関数はlistなんで、




(defparameter *lst* (list 1 2 3 4 5))


って書いてもいい筈。でも書かない。何故なら前者の方が短くリストを設定出来るから、です。後者は長い。よって前者のパターンばっか書く確率が高いでしょう。そしてある意味、基本関数である筈のlistはもっとも使われない関数となってしまうんです。



ところが、一見似た結果をもたらす両者なんですが、次のようにしてみると当然結果が違うんです。




CL-USER> (defparameter *quoted-lst* '(1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9)))
*QUOTED-LST*
CL-USER> *quoted-lst*
(1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9))
CL-USER> (defparameter *listed-lst* (list 1 (+ 2 3) (- 4 5) (* 6 7) (/ 8 9)))
*LISTED-LST*
CL-USER> *listed-lst*
(1 5 -1 42 8/9)
CL-USER> (equal *quoted-lst* *listed-lst*)
NIL
CL-USER>


ここは普段意識しないでしょうから結構重要です。特殊オペレータquoteは含まれたS式の評価を完全に止めます。しかしながら、listは関数なんで最初に引数を評価してから作用する。従って、listの引数にS式を与えたら、それぞれの引数の計算結果が返ってくるんです。当たり前なんですがかなり重要な事です。



Lispのプログラムはリスト



さて、上で見た通り、関数listと特殊オペレータquoteの動作は丸っきり違うわけですけど、この二つを組み合わせるとS式を部分評価してS式を組み立てられる事に気づきます。例えば次のようにして。




CL-USER> (let ((x 0))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP 0) 'ZERO) ((PLUSP 0) 'POSITIVE) (T 'NEGATIVE))
CL-USER>


恣意的な例なんですが、Lispのプログラムを返してるのが分かるでしょうか。評価が成されてない、入力で書きそうなS式が返っています。しかも、必要な部分(この場合はx)が評価されて埋め込まれています。別な言い方をすると確かにコードを生成してるんです。あるいはテンプレートを生成した、と言うべきか。




CL-USER> (let ((x 1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP 1) 'ZERO) ((PLUSP 1) 'POSITIVE) (T 'NEGATIVE))
CL-USER> (let ((x -1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative)))
(COND ((ZEROP -1) 'ZERO) ((PLUSP -1) 'POSITIVE) (T 'NEGATIVE))
CL-USER>


しかし、これらは評価されていません。実際に評価を下すのがLisp万能関数evalです。




CL-USER> (eval (let ((x 0))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
ZERO
CL-USER> (eval (let ((x 1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
POSITIVE
CL-USER> (eval (let ((x -1))
(list 'cond (list (list 'zerop x) ''zero)
(list (list 'plusp x) ''positive)
(list t ''negative))))
NEGATIVE
CL-USER>


つまり、defmacroの仕組みとは、基本的には、



  1. マクロの記述形式を引数として受け取る。

  2. それをリストで記述されたコードのテンプレートに受け渡す。

  3. 最終的にevalを適用する。



と言う事です。まあ、実際はこんなに単純に動作しているわけじゃないんでしょうが、「考え方」はこの通りですね。なお、簡単なdefmacroの実装方法に関してはOn Lispマクロのモデルに記載されています。



この考え方でifを定義すると、次のようになります。




(defmacro my-if (pred? then-clause else-clause) ;マクロの記述形式を引数で表現する
(list 'cond (list pred? then-clause) ;リストで組み立てられたコードのテンプレートを記述する
(list t else-clause)))


my-ifは次のような再帰的定義内でもキチンと動く事が分かります。




CL-USER> (defun fact (n &optional (acc 1))
(my-if (zerop n)
acc
(fact (1- n) (* n acc))))
FACT
CL-USER> (fact 3)
6
CL-USER>


バッククオートとカンマ



マクロの基本的な書き方は上の通りです。歴史的には、元々実際上のようにして書いてたらしいんですがlistlistlist、だと見づらいです。かつ書きづらい。そこで、listquoteを使う代わりに、この二つの組み合わせが行う事を別の書き方で表現する方法がバッククオート(※1)(日本語キーボードだとShift-@)とカンマなんです。












listquoteを使った評価例 バッククオートとカンマを使った評価例

CL-USER> (list 'a '(+ 1 2) (+ 3 4))
(A (+ 1 2) 7)
CL-USER>

CL-USER> `(a (+ 1 2) ,(+ 3 4))
(A (+ 1 2) 7)
CL-USER>


これらは写真で言うポジとネガの関係のようです。同じ事を書き表すのに反転しているように見える。


listの場合は引数で評価したい部分式はそのまま、評価を止めたい部分式にクオートしますが、バッククオートでは評価したい部分式にカンマを付けて、評価を止めたい部分式をそのまま記述します。いずれにせよ、結果は同じになります。



また、カンマは上のようにバッククオートの中で使われるのが前提なんで、カンマ単独で使用しようとする(※2)と、必ずエラーを返します。カンマはバッククオートの一部です(これは元々listの代用表記だと言う事を考えてみても分かるでしょう)。




;;; エラーの例
;;; 「カンマがバッククオートの中にないよ!」と文句を言ってくる
CL-USER> '(+ 1 ,(+ 2 3))
SB-INT:SIMPLE-READER-ERROR on #<SB-IMPL::STRING-INPUT-STREAM {AF98ED9}>:
comma not inside a backquote
[Condition of type SB-INT:SIMPLE-READER-ERROR]
; Evaluation aborted.
CL-USER>


そして、上のエラーの例見ても分かるでしょうが、クオートとバッククオートは紛らわしいです。間違えないようにしましょう。もう一回書きますが、日本語キーボードだとバッククオートはShift-@です。



my-iflistで組み立てられたヴァージョンとバッククオートで組み立てられたヴァージョンを並べて見てみます。












listとクオートで書いたmy-if バッククオートとカンマで書いたmy-if

(defmacro my-if (pred? then-clause else-clause)
(list 'cond (list pred? then-clause)
(list t else-clause)))

(defmacro my-if (pred? then-clause else-clause)
`(cond (,pred? ,then-clause)
(t ,else-clause)))


若干見やすくなってスッキリしている事が分かると思います。若干ですがね。




※1: あるいはそのまま逆引用符、等と呼んだりする。

なお、Schemeでは`quasiquote準引用符等と呼んで、バッククオートとは呼ばない。同じものを別の呼び方で呼ぶ。文化圏が違うのである。

加えて、quasiquoteはR5RSでも定義されているが、マクロが衛生的マクロしか定義されてないので、仕様書範囲内では何のために存在してるんだかサッパリ、である。単純にそれこそlistの代用としてしか使い道がない。仕様書の範囲内では。

※2: Emacs + SLIME(あるいはLispbox)では、REPLでカンマを丸裸で打つと、SLIMEのコマンドが列挙される。つまりSLIME上ではカンマはSLIMEの機能の呼び出しコマンドにあたる。従って、REPL上ではカンマが丸裸では使えないように設計されているので、ミスは減る。


macroexpand-1



若干と強調したのは、どのみちバッククオートがlistの代用である以上、見た目はスッキリしたとしてもlistでコードを組み立てるメンド臭さは変わらないと言う事だから、です。そして、メンドくさい、って事はいずれにせよ間違える



Schemeだったらコード記述時に間違えた場合、処理系は知らんぷりなんですけど、そこは過剰な親切設計であるANSI Common Lisp。マクロ記述用の一種のデバッガまで用意しています。macroexpand-1と言う関数がそのデバッガにあたります。




CL-USER> (macroexpand-1 '(my-if (zerop n)
acc
(fact (1- n) (* n acc))))
(COND ((ZEROP N) ACC) (T (FACT (1- N) (* N ACC))))
T
CL-USER>


上の例はmy-ifを用いて定義した階乗関数の一部分をmacroexpand-1に手渡したものです。macroexpand-1は関数なので、受け渡すフォーム(作成したマクロを使った部分コード)はクオートしなければなりません。しかし、それさえ守れば、コードが意図したように展開されたかどうか、一発で分かりますね。問題があったら修正、と言う流れです。



@valvallowさんがブログで記述していた宿題のうち、whenunlesspoppushなんかは仕様で定義されているマクロなんで、全部macroexpand-1で展開形を見てみて、カンニングする事が出来ます(笑)。




CL-USER> (macroexpand-1 '(when t 'hello))
(IF T (PROGN 'HELLO) NIL)
T
CL-USER> (macroexpand-1 '(unless t 'hello))
(IF T NIL (PROGN 'HELLO))
T
CL-USER> (macroexpand-1 '(pop stack))
(LET* ((#:NEW769 STACK))
(PROG1 (CAR #:NEW769) (SETQ #:NEW769 (CDR #:NEW769)) (SETQ STACK #:NEW769)))
T
CL-USER> (macroexpand-1 '(push 1 (car lst)))
(LET* ((#:G772 1) (#:TMP771 LST) (#:NEW770 (CONS #:G772 (CAR #:TMP771))))
(SB-KERNEL:%RPLACA #:TMP771 #:NEW770))
T
CL-USER>


#:とか言うのは変数名なんで、若干見にくいんですが、要するにxとかyとかの無意味な変数名と同じものです。他には、上の例の場合、SB-KERNEL:と記述されているのはSBCLの実装上、プリミティヴとして定義されている関数の事を表したりしてるんですが、いずれにせよ、それらの「独特の表記」さえ抜かせば、解読はさほど難儀ではないと思います。



いずれにせよ、ツマッた場合のカンニングは、学校のテストじゃご法度ですが、CLのマクロでは推奨されています。




マクロ理解のポイントは、それがどのようにして実装されているかを理解することである。実質的にマクロは式を変換する関数に過ぎない。



マクロ展開は単なるデバッグの補助手段ではなく、マクロの書き方の勉強手段でもあることを言っておきたい。Common Lispには100以上の組み込みマクロがあり、なかには大変複雑なものもある。そんなマクロの展開形を見ることで、それらがどう書かれたのかが分かることも多い。




destructuring-bind、カンマアットと&body



同じく、@valvallowさんのブログで示唆されている宿題ではforがあります。しかし、一般にforとは言っても、





からはじまって、色々な形式のforがあります。言語によって形式的には全く違う。



そこで、もっともメジャーだと思われるC言語のforのスタイルを借りてきます。C言語嫌いの僕でも一応K&Rは持っているんで(笑)、そこから形式を借りてきます。




for

for (expr1; expr2; expr3)




さいでっか(笑)。何とシンプルな(笑)。


ここで、はCommon Lispで言う本体部(body)、になるでしょうね。あとはCommon Lispの組み込みマクロであるdoを使って、次のようにしてでっち上げてみます。




(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))


これでK&Rの冒頭にある、華氏->摂氏の変換表も次のように、Cスタイルで簡単に書けますね。




CL-USER> (for ((fahr 0) (<= fahr 300) (incf fahr 20))
(format t "~A ~A~%" fahr (float (* 5/9 (- fahr 32)))))
0 -17.777779
20 -6.6666665
40 4.4444447
60 15.555555
80 26.666666
100 37.77778
120 48.88889
140 60.0
160 71.111115
180 82.22222
200 93.333336
220 104.44444
240 115.55556
260 126.666664
280 137.77777
300 148.88889
NIL
CL-USER>


さて、上のforは実は次のようにしても書けるんですが、




(defmacro for (exprs &body body)
`(do ((,@(car exprs) ,(third exprs)))
((not ,(cadr exprs)))
,@body))


好みにもよるんですが、若干見づらいですね。比べてみますか。












最初のヴァージョンのfor 次のヴァージョンのfor

(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(defmacro for (exprs &body body)
`(do ((,@(car exprs) ,(third exprs)))
((not ,(cadr exprs)))
,@body))


平たく言うと、ANSI Common LispではSchemeの衛生的マクロ程明らさまで強力なパターンマッチングの機能は無いんですが、一方、単純なパラメータの分配に関して言うと、行う事が出来ます。この機能をdestructuring-bindと呼びます。




Common Lispのdefmacroではパラメータリストは任意のリスト構造であってよい。マクロ呼び出しが展開されたとき、マクロ呼び出しの構成要素はマクロのパラメータにdestructuring-bindと同様に代入される。




「任意のリスト構造で良い」と言うのは次のような事です。つまり、最初のヴァージョンのforでは、expr1は二つの要素を持つリストだと仮定していました。よって、三つ以上の要素を持つリストがexpr1に渡されたら明らかにバグります。エラーチェックを行ってないわけです。


そこで「任意のリスト構造で良い」と言うのなら、これを避ける意味もあって、forは次のように記述しても構わない、と言う事です。




(defmacro for (((var start) expr2 expr3) &body body)
`(do ((,var ,start ,expr3))
((not ,expr2))
,@body))


もう一回並べておきますが、もうちょっとforの動作が明確になってるんじゃないか、と思います。












最初のヴァージョンのfor 最後のヴァージョンのfor

(defmacro for ((expr1 expr2 expr3) &body body)
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(defmacro for (((var start) expr2 expr3) &body body)
`(do ((,var ,start ,expr3))
((not ,expr2))
,@body))



控え目に使う限り、パラメータリストの分配は明確なコードにつながる。~(中略)~ 本体の式の前に複数の引数を取るようなマクロで便利だ。




となると、カンマアット(,@)(※)が何を行っているのか明確です。それはつまり、次のような作用がある、と言う事です。




CL-USER> `(,@(list 'var 'start))
(VAR START)
CL-USER> (let ((expr1 '(fahr 0))
(expr2 '(<= fahr 300))
(expr3 '(incf fahr 20))
(body '((format t "~A ~A~%" fahr (* 5/9 (- fahr 32))))))
`(do ((,@expr1 ,expr3))
((not ,expr2))
,@body))

(DO ((FAHR 0 (INCF FAHR 20)))
((NOT (<= FAHR 300)))
(FORMAT T "~A ~A~%" FAHR (* 5/9 (- FAHR 32))))
CL-USER>



カンマアット(,@)はカンマの変種で、機能はカンマと同じだが違いが1点ある : 次に続く式の値をカンマのようにそのまま挿入するのでなく、カンマアットは切り張り操作を行う。つまり、1番外側の括弧を取り除いて挿入する :




残るは&bodyですが、これはレスト・パラメータ指定、つまり&restと基本的には同じです。ただし、macroexpand-1等を行った際に改行やインデントを施したプリティ・プリント(清書印字を)してくれるかどうかが差、なんですが、現時点、処理系によってはどっちでもプリティ・プリントを行ってくれる模様です。だから慣習的な意味しかない、と言えば無いでしょうね。関数を書く場合は&restを使って、マクロを書く場合は&bodyを使う、程度の差しか無いと言えば無いでしょう。


レスト・パラメータは以降の式をすべて纏めて単一リストにしちゃうので、本体の実行部を全てマクロ定義内に並べる為には一番大枠の括弧が要らなくなるので、当然カンマアットが必要になるわけです。




※: ちなみに、カンマアットが正式名称なのか、と言うとそれは分からない。CLHSを見る限り、バッククオートに付いての章しかなくて、

    カンマに続いてアットサインがある場合は…

のように記載されているので、特にハッキリとした名称は存在しない模様である。ただし、歴史的には通称カンマアットで間違いはない。

ちなみにSchemeのR5RSでは、,unquote,@unquote-splicingと言う洒落た名称が与えられている。


カンマ、カンマアットとmapcar



Schemeの衛生的マクロの場合、省略符号(...)とパターンマッチがあるお陰で、簡単なパターン記述がしばしば行えるわけですけど、ANSI Common Lispのマクロにはそう言う洒落た機能はありません。もう一回最初の例に戻りますが、












Schemeの衛生的マクロでのmy-let ANSI Common Lispのマクロでのmy-let


(define-syntax my-let
(syntax-rules ()
((_ ((x v) ...) body ...)
((lambda (x ...)
body ...)
v ...))))

(defmacro my-let

(bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


でScheme版だと省略符号で上手く回避しているパターン記述に対して、ANSI Common Lispでは与えられたリスト(この場合はbindings)の要素を操作して切り張りしているのはそれが理由です。カンマやカンマアットに続く部分は部分評価が成されるので、バッククオートで形作られたテンプレートにその評価結果をハメこんでいるわけです。


これはANSI Common Lispのマクロに於いて良く見かけるテクニックなんで覚えておいた方が良いと思います。かつ、悪名高いネストされたバッククオートと言うのもこれ絡みで良く出てくる現象です。


なお、必ずmapcarじゃないといけないのか、と言うとそう言うわけでもなくって、例えば実践Common Lispの著者であるPeter Seibelはmapcarよりloopを使うのが好みのようです。












mapcarを使ったmy-let loopを使ったmy-let

(defmacro my-let (bindings &body body)
`((lambda ,(mapcar #'car bindings)
,@body)
,@(mapcar #'cadr bindings)))


(defmacro my-let (bindings &body body)
`((lambda ,(loop for i in bindings collect (car i))
,@body)
,@(loop for i in bindings collect (cadr i))))


どちらでもお好きなように。展開形が同じなら、スタイルの違いは大した問題じゃない、と言うのがCommon Lispらしさと言えばらしさです。



二度あることは三度ある



以上がCLのマクロに付いての基本的な概観の全てです。



ところで、LOLでは次の二つの関数が紹介されています。












G-BANG-SYMBOL-PREDICATE O-BANG-SYMBOL-PREDICATE

(defun g!-symbol-p (s)
(and (symbolp s)
(> (length (symbol-name s)) 2)
(string= (symbol-name s)
"G!"
:start1 0
:end1 2)))

(defun o!-symbol-p (s)
(and (symbolp s)
(> (length (symbol-name s)) 2)
(string= (symbol-name s)
"O!"
:start1 0
:end1 2)))


実は殆ど全く同じです。こう言う場合、IDEだとコードをコピペして編集、ってのが一つのやり方なんですが、似たような関数だったらマクロを書いて関数を自動で生成しちゃうと言うのも手です。もちろん、この二つは目的を持って作られた関数なんですが、二度ある事は三度あるとも言いますしね(※)。今後a!-symbol-pなんて関数が欲しくならない、と言う保証もないんで、関数*!-symbol-pを生成するマクロを最後に作ってみましょう。




(defmacro !-symbol-p-generator (str)
;; 最初に材料の下処理をやっておく
(let ((len (1+ (length str))))
;; !が付くところの生成と関数名の生成をしておく
(let ((bang-part (string-upcase (concatenate 'string str "!"))))
(let ((func-name (intern (concatenate 'string bang-part "-SYMBOL-P"))))
;; マクロ本体
`(defun ,func-name (s)
(and (symbolp s)
(> (length (symbol-name s)) ,len)
(string= (symbol-name s)
,bang-part
:start1 0
:end1 ,len)))))))


Schemeの衛生的マクロとANSI Common Lispのマクロのおそらく最大の違いは、Common Lispではマクロのテンプレートを生成する前に下処理が出来る辺りです。これがR5RSのマクロでは出来ません。上のマクロ、!-symbol-p-generatorでは、"!"が付く部分(bang-part)と、関数名(func-name)を最初に生成しておいて、マクロのテンプレートにそれらをハメこんでいます。



注意点としては、internは大文字から成る文字列はそのまま素直にシンボルに直してくれるんですが、小文字が混在する場合、|(縦線)込みのシンボルにしてしまいます。




CL-USER> (intern "MY-SYMBOL")
MY-SYMBOL
NIL
CL-USER> (intern "My-Symbol")
|My-Symbol|
NIL
CL-USER>


ANSI Common Lispはcase-insensitiveなプログラミング言語でした。よって、internで関数名(シンボル)を生成する前に、引数として与える文字列を全て大文字にしておきましょう。



!-symbol-p-generatorは文字列を引数として取って関数"*!-symbol-p"を返すマクロです。macroexpand-1g!-symbol-po!-symbol-pが生成されてるのかどうか確認してみましょう。




CL-USER> (macroexpand-1 '(!-symbol-p-generator "g"))
(DEFUN G!-SYMBOL-P (S)
(AND (SYMBOLP S) (> (LENGTH (SYMBOL-NAME S)) 2)
(STRING= (SYMBOL-NAME S) "G!" :START1 0 :END1 2)))
T
CL-USER> (macroexpand-1 '(!-symbol-p-generator "o"))
(DEFUN O!-SYMBOL-P (S)
(AND (SYMBOLP S) (> (LENGTH (SYMBOL-NAME S)) 2)
(STRING= (SYMBOL-NAME S) "O!" :START1 0 :END1 2)))
T
CL-USER>


上手い具合にコードが生成されている模様ですね。実際動かしてみましょうか。




CL-USER> (!-symbol-p-generator "g")
G!-SYMBOL-P
CL-USER> (g!-symbol-p 'hoge)
NIL
CL-USER> (g!-symbol-p 'g!hoge)
T
CL-USER> (g!-symbol-p 'o!hoge)
NIL
CL-USER> (!-symbol-p-generator "o")
O!-SYMBOL-P
CL-USER> (o!-symbol-p 'hoge)
NIL
CL-USER> (o!-symbol-p 'g!hoge)
NIL
CL-USER> (o!-symbol-p 'o!hoge)
T
CL-USER>


もちろん、!-symbol-p-generatorは一文字以上の文字列も受け取って*!-symbol-pを生成出来ます。




CL-USER> (!-symbol-p-generator "bang")
BANG!-SYMBOL-P
CL-USER> (bang!-symbol-p 'hoge)
NIL
CL-USER> (bang!-symbol-p 'g!hoge)
NIL
CL-USER> (bang!-symbol-p 'o!hoge)
NIL
CL-USER> (bang!-symbol-p 'bang!hoge)
T
CL-USER>


こうなってくると楽しくなってきますね(笑)。




※: 個人的な経験では、小さなREPLを書いてる時良く起こります。例えばアドヴェンチャー・ゲームの入力/表示部分とか。ちょっとだけ内容が違うのに、形式的には全く同じ関数を何度も書いてる事に気づいて嫌になりました。

C言語でやると大体コードがscanf/printfまみれになって、それこそIDEでコードを大量にコピペするケースでしょうが、LispだったらマクロでREPLの概形を書き下して、マクロに関数を自動生成させた方が手っ取り早かったりします。修正するなら生成側のマクロ「だけ」を修正すれば良く、自動生成された関数は勝手に定義しなおされます。ある意味、オブジェクト指向のクラスの継承なんかより遥に強力でしょう。


Schemeの伝統的マクロ



取りあえずこれで #9LISP 014用のメモは全て、です。最後に実装依存ですが、




「やっぱSchemeがいいなあ~~。」


と言う人の為に、いくつかのScheme処理系での伝統的マクロの使い方を紹介しておきます。




  • Gauche: デフォルトでdefine-macroと言う形式で利用可能です。

  • PLT Scheme: (require mzlib/defmacro)を評価した後、define-macroと言う形式で利用可能です。

  • Guile: デフォルトでdefine-macroと言う形式で利用可能です。



この3つは形式的には全て同じなので、どれもGaucheプログラミング(立ち読み版)の示唆通り動くでしょう。




;;; PLT Scheme の場合
Welcome to MzScheme v4.2.5 [3m], Copyright (c) 2004-2010 PLT Scheme Inc.
> (require mzlib/defmacro)
> (define-macro (my-let varlst . body)
(let ((vars (map car varlst))
(exps (map cadr varlst)))
`((lambda ,vars ,@body) ,@exps)))
> (my-let ()
(+ 3 4))
7
> (my-let ((x 7)
(y 10))
(+ x y))
17
> (my-let ((x 3))
(my-let ((x 10)
(y (* x x)))
y))
9
>

;;; Guile の場合
guile> (define-macro (my-let varlst . body)
(let ((vars (map car varlst))
(exps (map cadr varlst)))
`((lambda ,vars ,@body) ,@exps)))
guile> (my-let ()
(+ 3 4))
7
guile> (my-let ((x 7)
(y 10))
(+ x y))
17
guile> (my-let ((x 3))
(my-let ((x 10))
(y (* x x)))
y)
guile> (my-let ((x 3))
(my-let ((x 10)
(y (* x x)))
y))
9
guile>


パラメータ・リストの記述方法がSchemeらしい、ですが、基本これでCommon Lisp相当のマクロを記述する事が可能です。

2 件のコメント:

  1. Scheme についてひとつだけ指摘。
    R5RS の2章冒頭で case-insensitive であることが明確に求められています。
    文字定数/文字列定数意外では大文字小文字を区別しないというのが R5RS でのルールです。

    返信削除
  2. >>斎藤さん

    >R5RS の2章冒頭で case-insensitive であることが明確に求められています。

    おお、そうですね。ご指摘ありがとうございます。見落としてました。

    っつー事は、やっぱり「仕様書に準じてない」処理系が多いって事でFA?

    返信削除