2014/09/02

On Lispを読んだ.

 Paul Graham(著), 野田 開(訳), On Lisp, オーム社, 2007を読んだ.

 今更感がありますが, 普段使う言語は大抵, Scheme(Gauche)/Clojureなので, Common Lispについてはあまり興味がありませんでした. 本書は, 大学の図書館で, 斜め読みした程度でした(どちらかといえば, Let Over Lambdaの方が好きでした)が, 今回はちゃんと読みました(コードはまだ動かしてないです).
 主な目的は, 主にマクロの部分です. 本書の7章から24(25?)章がマクロの説明にあてられています. 特に読みたかったのは, ATNを使ったパーサの章と継続の章, 汎変数のあたりです.
 というわけで, 以下の文章は, その時の感想です.


 1章から6章までは, 主にCommon Lisp入門といった内容で, 簡単にCLの使い方について説明されています.
 7章からいよいよ, マクロの話です. これを使うシチュエーションについて本書では, 7種類くらい挙がってますが, 私は, マクロの主な用途は,
  • 評価順序(回数, 条件など)の操作
  • 副作用
を使うときの2種類でいいのではないかと思います.(※あくまでも個人の感想です)

 大半のマクロは, 展開されるS式を短くDRYに書くためのものです. 関数で書くこととマクロで書くことによる記述の大きな違いは, 与えられた式が関数評価の順序で計算される(ノーマルフォーム)か, 与えられた式が特殊な評価順序で処理される(スペシャルフォーム)かの違いです. つまり, 特殊な評価順序で処理されることを目的とする部分が重要だと考えました.
 単純に冗長な記述を縮約するだけなら, 関数にまとめて書けばいので, この1点目の目的は, かなり大きいと思います.
 例えば, Lispのマクロで, yaccのようなパーサジェネレータ(マクロによる展開)を書くことはできると思われますが, パーサコンビネータ(関数合成による構文解析器生成)でもOKで, わざわざマクロで書く必要はないかもしれません. このような単純に展開する操作だけならば, 関数でも代用できるはずです (高速化や最適化のためには, インライン展開などの方法も考えられます).

 2つ目のマクロの用途が「副作用」だというのは, コンテクスト(環境の操作), 継続や例外(withマクロ系)など, これも関数では扱えないパターンを, 簡略化や集約するときの使い方のことです. これは「副作用」とひとまとめにすることができます. 副作用も基本的に関数では扱えない部分で, 代表格は, letやmatchとか, その派生系ですが, マクロ需要の重要な位置を占めていると思います.

 というわけで, 基本的に, マクロはDSL(Domain Specific Language)なので, DSLについて方法論の説明がOn Lispの内容の大半を占めているようにも読めました. 本書では, DSLは「埋め込み言語」と書かれています(ドメイン特化言語の方が一般的な訳語だとは思いましたが).

 (Common)Lisp内部でマクロとしてDSLを書く利点は, DSLをLisp本体のコードと一緒にコンパイルできるという点があると書かれていました.
 例えば, C言語においてDSLを書きたい場合, それ用のインタプリタ/コンパイラ(または変換器?)の実装が必要になりますが, Lispではその必要がありません. Common Lispの最適化やGCのインフラの恩恵をそのまま受けられるということでしょう.
 このあたりは, 今まで意識してきませんでしたが, Lisp特有のメリットだと思いました.

 9章では, 「変数捕縛」というタイトルですが, ひたすらマクロ展開における変数の重複の回避について説明するという内容です. 例えば, Common Lispの準クォートなどによる素朴なマクロ展開では, 変数名が重複してしまうケースがあります. 例えば, 以下の例.

`(funcall #'list ,a ,b)
というマクロ展開があるとします. labelsが関数名listへ別の関数を束縛した時, labelsの内側でこのマクロを展開すると,
(labels ((list (x y) y)) `(funcall #'list ,[aに相当する式] ,[bに相当する式]))
という式に広げられ, この内側のlistは, 直前で束縛された関数listです. これは本来の展開の意図と異なります.
(funcall #'list 1 2 3) ;; => (1 2 3)
(labels ((list (x y z) y)) (funcall #'list 1 2 3)) ;; =>2
くらい違います.

 Common Lispが長年, 衛生的マクロを導入したなかったことを考えると, 実用上問題はないように思っていました. 著者自身が「病的」とまで語るほど, 名前の衝突を避けるために1セクション割いていることを考えると, Schemeの衛生的マクロや, Clojureにおける, 準クォートでの名前空間付きの準クォートは妥当な戦略なのかなと思いました.

 「汎変数」はGeneralized Variableの訳語で, 同じPaul Grahamが著者のANSI Common Lispでは, 一般化変数と訳されていました. これは, マクロによるシンタックスシュガーです. (変数の破壊は好きではないので通常のsetq/set!もふくめて)使ったことがないのですが, Gaucheにもあるようです.

 consを徹底的に排除するような記述が多く, 関数型言語とは程遠い印象を持ちます. consは, 参照透過なリストを作るために常用される手段なので, 関数型プログラミングには欠かせない仕組みだと思っていたのですが.

 Schemeは割と関数型言語寄りな書き方が多いような印象がありますし, Clojureは, よく「関数型言語」と呼ばれます(変数を破壊するようなことは基本的になくて, STMなどが使われます). Lisp系の言語は, 副作用は排除しがちなのだと思っていたのですが, On Lispでは割と破壊的代入が常用されていて驚きました.

 全体的に速度(やその最適化)を意識した内容になっていて, 少し意外でした.
 Scheme(Gauche)やClojure関連の話題で, 速度やそのチューンナップを意識した話はあまり聞かないような気がします. 私が寡聞なだけかもしれませんが.
 HaskellにはCalculational Programmingというのがあり, 高速化に関してもかなり抽象的な印象があります. 一方,  Common Lispにおける高速化はそれと真逆で, どちらかといえば, consを使わない(動的メモリ確保を減らす)とか, マクロによりインライン展開などの効果に由来するものが多かった印象です.

 私が特にCommon Lispいいな, と思ったのは, リードマクロの部分です. 本書では17章にあたります. これは, プログラム中の文字数を効果的に削減できるものだと思います. 例えば, リードマクロにより,
#[1..10] ;; => (1 2 3 4 5 6 7 8 9 10)
みたいな展開をするマクロが書けるんですね. Clojureでも使えないか探してみたところ, Clojureのリーダマクロ(記事後半部の「ディスパッチマクロの拡張」)のページを発見. これは遊んでみる必要があります...

 18章は「分配」と不思議なタイトルですが, これはよくあるmatchマクロのことでした. HaskellやOCamlでいうところのパターンマッチを実行するマクロですね.

 20章でいよいよ「継続」の話題に入るわけですが, ここでマクロ展開される継続は, CPS変換ではなく, グローバル変数の書き換えで継続を捕縛していました. Schemeの第一級の継続と比較すると見劣りしますが, 驚くべきことに, この実装が次の2つの章, 21章「マルチプロセス」と22章「非決定性」で実際に用いられた上に, 23章におけるATNパーサや25章のProlog実装につながります.

そういえば, web上に資料は上がっていたんですね.
On Lisp (Paul Graham著,野田 開 訳)

(2015/02/09 : 一部修正)

0 件のコメント :