On Lispとか読んでRubyとLispの違いをちょっと考えた
全400ページのうち3/4がマクロの説明に当てられている、Paul Grahamの「On Lisp」を8割ほど読んだ。冒頭のCommon Lisp入門で紹介されている標準関数やユーティリティ関数のネーミングのひどさ(やたらと数字でサフィックス付けるとか、rplacaとかってどうよ?)や、破壊的操作をする関数の挙動の不統一感に、「これならRubyのほうがいいんじゃね?」とか思ったし、with系マクロも、「なんだRubyのブロックでいいじゃん」とか思ったりもしたけど、確かに読み進めると全然違う世界が垣間見えてきた気がする。
CLOSの説明の導入編に当たるところで、Paul Grahamは素のLispの上にオブジェクトシステムを作ってみせる。これが想像以上に簡単な話で、そうか、なるほど、ハッシュテーブルとクロージャがあれば、大抵のことはできるのだと、ちょっと驚いた。継承のモデルにしたって、メソッドというかシンボルのルックアップをどうたどるかという話だし、そりゃ出来て当たり前かと思うんだけど。OO言語をプロトタイプベース、クラスベース、多重継承、単一継承とか、いろいろ分類するけど、そうしたバリエーションをすべて実行効率の妥協なしに実現できるのだから、Lispを一般的なOO言語と比較するのは、そもそも間違いなんだろう。
例えばいま、RubyではRuby 2.0に向けて、既存クラスへのモンキーパッチの影響力を限定すべく、スコープを限定したり、mixinのときに名前の衝突があった場合の挙動を変えようとしているけど、そういうのだって、Cじゃなくて、Lispの上にオブジェクトシステムを作っておいたら、実験や変更はずっと容易だったんじゃないだろうか、と思った。というか、CRubyの場合、Cの壁、コミット権の壁、合意の壁があって、そういう変更は極めて難しそう。
On Lisp全体を通して思ったけど、効率について常に言及があって、RubyやRailsとえらく違うなと思った。所詮、Ruby/Railsが成功してるのはWebスタックぐらいだよ、と批判したJavaの生みの親のジェームス・ゴスリンじゃないけど、確かに汎用プログラミング言語としてはRubyは遅いほうだし、2011年の今、Rubyistと呼ばれる人々は、Webアプリばかり書いているし、あまりパフォーマンスのことを言わない気がする。特にメソッド呼び出しのオーバーヘッドは、全く無視する人が多いんじゃないだろうか。そういう領域でしかRubyは使われてないのかもしれない。
もう1つ、Rubyのコアクラスを使っていれば効率は「良いだろう」と想定できるってことがあるのかも。各種メソッドはCハッカーが注意深く実装していて、アルゴリズムや計算量についても、ちゃんと考えてくれていそう、という。そして、既存のクラスライブラリで足りないものがあって、自分でCommon Lispでいうところの「ユーティリティ関数」を足したくなるかというと、どうも良く分からない。今どきの言語は最初からユーティリティ関数てんこ盛りでデカい。
アナフォリックマクロに、ちょっと感動した。アナフォラというのは「it」のように既出の何かを指し示す自然言語の語彙。これを応用した「aif」は「if」とほぼ同様の挙動だけど、ifの条件節が返す値を「it」というシンボルにバインドする。そういえばRSpecとかいうRubyのライブラリでもitが出てくるな。
(aif (big-long-calculation) (foo it))
これはマクロで展開されて、
(let ((it (big-long-calculation))) (if it (foo it) nil))
となる。以下のようにやってみたらEmacs Lispでも動いた。意味のないコードだけど。
(defmacro aif (test-form then-form &optional else-form) `(let ((it ,test-form)) (if it ,then-form ,else-form))) (defun double-if-odd (n) (if (oddp n) (* n 2))) (defun print-num (n) (aif (double-if-odd n) (message "%d" it))) (mapcar 'print-num '(1 2 3 4 5 6 7 8 9)) => ("2" nil "6" nil "10" nil "14" nil "18")
もう1つ、アナフォリックマクロの例としては、無名関数の再帰のときに、コッソリとselfというラベルを付けて、再帰呼び出しのときに「(self hoge foo)」などとするもの。面白い。
マクロ展開の後に、意図しない形で変数を補足してしまうことがありえて、その根本的な対策として処理系にユニークであることを保証するシンボルを生成させる仕組みがあるというのにタマゲタ。「〜には〜の問題がある。このためCommon Lispには〜がある」というのがあっちこっちにあって、それが対症療法的に感じられなくもない。あ、なるほど、Schemerがいう、hygienic macroというのは、この問題のことだったのか。Wikipediaの記述には、Hygienic transformationという戦略にも言及がある。
Rubyでアナフォリックなifが使えたらどうろうだ。例えば、Railsのコードを「if.* =」で検索してみたら、以下のような箇所がある。
def redirect_to(options = {}, response_status_and_flash = {}) #:doc: if alert = response_status_and_flash.delete(:alert) flash[:alert] = alert end if notice = response_status_and_flash.delete(:notice) flash[:notice] = notice end if other_flashes = response_status_and_flash.delete(:flash) flash.update(other_flashes) end
もし、Rubyで「aif」を定義できたら、これらは、
def redirect_to(options = {}, response_status_and_flash = {}) #:doc: aif response_status_and_flash.delete(:alert) flash[:alert] = it end aif response_status_and_flash.delete(:notice) flash[:notice] = it end aif response_status_and_flash.delete(:flash) flash.update(it) end
などとなる。Railsの中では、オプションのハッシュから特定のオプション文字列を取り出すというコードがいたるところにあって、そういうところは、
def _normalize_callback_options(options) if only = options[:only] only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ") options[:per_key] = {:if => only} end if except = options[:except] except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ") options[:per_key] = {:unless => except} end end
という風になっている。これらも、aifを使えば、
def _normalize_callback_options(options) aif options[:only] it = Array(it).map {|o| "action_name == '#{o}'"}.join(" || ") options[:per_key] = {:if => it} end aif options[:except] it = Array(it).map {|e| "action_name == '#{e}'"}.join(" || ") options[:per_key] = {:unless => it} end end
と書ける。というか、そもそもoptionsというハッシュから取り出すパターンがあまりにも多いので、
if some_option = options[:something] process some_option end
は、
ext-if options[:something] do process it end
とか書けるといいんじゃないかしら。あ、これはマクロがなくてもRubyで定義できそう。あいや、こうやってブロックを使うという発想がマクロによる構文変換のレベルでものごとを考えていないってことか。マクロ的な自由度があれば、
ext-if { options[:something] : process it }
とか、
ext-if { options[:something] : ary << it}
とかできるのかな。
「aif」をRubyで定義するには、トップレベルにメソッドを加えるようなやり方ではダメで、これは関数では定義できない。たぶん、parse.yを書き換えてRubyの中身に手を入れることになる。それに、いきなり暗黙的に「it」がコードに登場する文法ってどうよという議論も当然あり得るので、この文法がRubyに入る可能性は極めて低そう。
新しい文法を入れること、しかも、ちょっと変態的な挙動の文法を入れるにしては、得られる効果が小さすぎる。
なるほど、Lispにはそうした制限がない。自分自身や、自分のチームが合意できるなら、この程度の拡張は別に何ということはないんじゃないだろうか。
「Rubyは受け入れられるLispだ」という主張の背景には、活発なコミュニティによるライブラリ開発の厚みがある(これはこれで、RubyのライブララリってRailsの似たようなプラグインとテスト関連ばっかしで、実は厚みなんかないという批判もあるようだ)。
Hacker Newsのこのスレッドを読むと、どうも、ここに登場するRubyistが劣勢に見える。「LispにしかできなくてRubyにできないことって具体的に何? 効率以外でさ」とか「Rubyでは全然Rubyに見えないDSLが作れる。こんな風に」とか反論してるけど、どうも的外れの印象が強い。
Lisperの1人は、以下のRubyのDSLについて、あちこちにRubyっぽさが認められると言ってる。
get / 'orders' >> :orders > :index with route / :slug / 'order' >> :orders do get > :show put > :update end get {'item_images' => :controller} / :image > :show
ぼくもそう思う。これは、かなり過激なほうで、不等号の演算子なんかどうやって処理してるのか気になるところだけど、やっぱり随所にRubyっぽさがある。「Rubyを拡張している」という感じではない。ところが、RubyistはこれのどこがRubyっぽいんだ、誰もお前に同意しないと思うよと反論していて、議論がかみあわない。なるほど、この議論のやり取りについてはPaul Grahamの言うとおりかもしれない。より高い抽象レベルで物事を捉えている人と、そうでない人とで議論がかみあわないのは当然だ。
そうか、ActiveSupportにしろ、ActiveRecordのDSLにしろ、クラスを拡張したり、既存クラスにゴニョゴニョっと、人間が読み下しやすいメソッドを足したりしているだけで、Rubyを拡張なんてしてないんだな。RubyがDSLに向いていると言われるのは、読み下しやすい語彙をノイズが少なく並べやすいというあたりに理由があるわけだけど、それは元々、メソッド呼び出し時にカッコを省略できるだとか、ハッシュでシンボルを渡すコンベンションだとか、それはRubyそのものなわけだし。