#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
なんて使う気にならなくなるから。」
とかとても能天気な事を書いています(笑)。
実際、狙ったマクロ「だけ」を展開出来て、一見優れものに見えるんですが、
と三重苦です。マジな話やってられません。
Common Lisp実装
んで、次回までANSI Common Lisp処理系を入れようと言う話です。 #9LISP で話に上がったANSI CL処理系は次の4つ。メモしておきます。
とまあ、主観交えて書けばこんな感じでしょうか。お勧めはPC-UNIXだったらSBCL、Windowsだったら素直にCLISPにしとけば基本問題無い、と思います。Macは良く知らん。
どの実装を選ぶか、ってのは頭が痛い問題なんで、ぶっちゃけ、オールインワンのLispbox入れときゃ充分なんじゃねえの?とか思います。その方が設定で面倒臭い思いしなくて済みますしね。
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 で利用するのと同じ用途に,シンボルを利用してもよい。
色々ゴチャゴチャと書いていますが、要するに、
- 各シンボルはプログラム中で「唯一無二の存在」でなければならない。
- じゃないと識別子として役に立たない。
- つまり、シンボルは「名前を識別する為だけに」存在する。
と言ってるんですね。言わば
当たり前の事を言ってるんですが。他の言語鑑みてもわざわざ
シンボル型なんてデータ型が存在しなくても良いくらいアッサリしています。
ところが、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
であって、 package
に intern
したり 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の間でこの手の「バカに豊富な機能のどれを使うか?」と言うので意見が分かれる事が良くあるようで、例えば簡単なトコでは等価述語であるeq
とeql
に対しても「何でもeq
派」と「何でもeql
派」に分かれているらしい。前者はスピード重視派、後者はデフォルト推奨派、ならしい。
ちなみに、Emacs Lispにはdefparameter
やdefconstant
は存在しない模様でビックリした。恐らくこの中では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ではnull
とnot
は同機能異名の関数なんですが、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ではこの性質が大活躍します。上の例だと、and
がnil
を見つけると即刻評価を中止してnil
を返すのが肝なんですけど、lat
はリストである事が前提で、かつ、そこに対して特殊な判定は必要ないわけです。lat
が空リストになった途端、and
はそれを偽値と解釈して再帰は滞り無く終わる。
上の例は単純ですが、色んな場面で効いてくる、Common Lispの便利な側面です。
偽と空リストに対して同じものを使うと混乱を引き起こすことがときどきあるが、長年のLispプログラミングの中で私はそれが掛け値なしの勝利であることを確信している。なぜなら、空リストは集合論での偽であり、多くのLispプログラムは集合で考えるからである。
ANSI Common Lispのcar
とcdr
さて、偽値=空リストであるANSI Common Lispのnil
なんですが。Schemeと違って次の大変美しい性質があります。それは空リストにcar
やcdr
を適用したらどうなるか、と言う話です。
Schemeの場合、空リストにcar
やcdr
を適用するとエラーを返してきます。
> (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>
nil
にcar/cdr
を適用するとnil
が返る。これは大変ありがたい仕様です。
これは実はInterlisp由来の仕様だそうなんですが、いずれにせよ大変便利で、それがCommon Lispの仕様として取り入れられたそうです。
ANSI Common LispとSchemeのcar/cdr
の違いは、そのままそれらを利用した組み込み関数/手続きの挙動にも影響しています。代表的なのは次の機能でしょうか。
Scheme | ANSI Common Lisp |
list-ref | nth |
list-tail | nthcdr |
両者とも引数の順序が違うんですが、大体同じ機能です。ただし、nil
のcar/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 Schemerにrember-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入門から解説を引っ張ってきます。
補助変数とは関数内で局所的に使用する変数のことで、関数の引数とは関係ない。局所変数は、let
やlet*
などのスペシャル・フォームを使っても宣言できるが、ラムダ・リスト・キーワード&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にはmemq
、memv
、member
と言う組み込み手続きがあります。それぞれ二つ引数を取り、第二引数はリストで、第一要素と等価なcar
を持つ第二引数の部分リストを返します。ただし、この「等価」の定義が違って、memq
はeq?
、memv
はeqv?
、member
はequal?
を比較に用います。
> (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ではassq
、assv
、assoc
が用意されています。
> (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/member
、assq/assv/assoc
は共にMITのMacLisp由来で、Schemeでの名称もこれを受けています。一方、約10年後に登場したCommon Lispではキーワード・パラメータの導入と共に、積極的に統合化へ進んだ、と見た方が良いでしょう。
なお、MacLisp直系の子孫であるEmacs Lispでは、やはりSchemeのようにmemq/memv/member
、assq/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>
ちなみに、その他の変換手続きへの対応表は以下の通りとなっています。
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で使えるtrace
はSLIBのものです。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
で終わる、とされていますが、仕様内で定義されている述語を見ると、必ずしもそうじゃありません(例:atom
やnull
)。また、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
を実装しろ、って課題があったとします。教科書的にはlet
はlambda
の構文糖衣なんで、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)))
|
比べてみると、
- Schemeの衛生的マクロの方が
(syntax-rules () ...)
の為に記述要素が多い。
- Schemeの方が明示的なパターン変換を指定する為、括弧が多い。
- ANSI Common Lispの
defmacro
のパラメータ・リストが衛生的マクロによる記述パターンに対応している。
- 他はこのレベルでは構成自体は大して変わらない。
と言う事が見て取れます。概形自体は全く同じなんです。
「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
の仕組みとは、基本的には、
- マクロの記述形式を引数として受け取る。
- それをリストで記述されたコードのテンプレートに受け渡す。
- 最終的に
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>
バッククオートとカンマ
マクロの基本的な書き方は上の通りです。歴史的には、元々実際上のようにして書いてたらしいんですがlist
、list
、list
、だと見づらいです。かつ書きづらい。そこで、list
とquote
を使う代わりに、この二つの組み合わせが行う事を別の書き方で表現する方法がバッククオート(※1)(日本語キーボードだとShift-@)とカンマなんです。
list とquote を使った評価例 | バッククオートとカンマを使った評価例 |
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-if
のlist
で組み立てられたヴァージョンとバッククオートで組み立てられたヴァージョンを並べて見てみます。
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さんがブログで記述していた宿題のうち、when
、unless
、pop
、push
なんかは仕様で定義されているマクロなんで、全部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-1
でg!-symbol-p
やo!-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処理系での伝統的マクロの使い方を紹介しておきます。
この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相当のマクロを記述する事が可能です。