Island Life

< 芋づる開発 | ピアノレッスン 3回目 >

2011/05/17

Scheme脳とCL脳

たまたまAllegro CLからLuceneベースの検索プラットフォームSolr http://lucene.apache.org/solr/ をたたく必要があって ライブラリを書いたのだけれど、ついでだからと思い立って 同時にGauche用のモジュールも書いてみた。

普段はSchemeとCLを行ったり来たりしているし、一方で書いたコードを もう一方に移植することもあるけれど、同じことをするモジュールを 同時進行で書くというは初めてで、なかなか面白かった。

入門用の簡単な課題を書くぶんにはSchemeとCLの違いはせいぜい 異なる方言くらいにしか見えないけれど、 現実的なものを書こうとすると、根っ子の哲学の違いが 実際のコードに無視できない影響を与えているのがわかる。 英語とフランス語くらい離れているかもしれん。

CLではライブラリからして、副作用ベースで動くものが多い。 XMLのシリアライズに使ったこれ https://github.com/franzinc/net-xml-generator もその典型的なパターンで、with-xml-generation マクロで 動的環境をセットアップして、その動的環境内でXML構築のDSLを 実行すると、それがセットアップ時に指定されたポートへと出力される。

一方Schemeでは、下請け関数がSXMLの断片を作って返し、 全部まとめた後でシリアライザに渡す、という関数的なスタイルの方が普通だと思う。

どちらの方式にも一長一短がある。CL方式は、 バッファリングが不要な場合はどんなに出力が大きくなってもメモリを圧迫しない。 Scheme方式では出力の大きさに比例したメモリを必要とする (手軽にlazyな木が構築できればいいんだけど。)

条件によって要素を出したり出さなかったりする場合、 CL式では制御の流れだけを考えて、どういうコンテキストにあってもwhenで囲めば良いが、 Scheme式では、その戻り値がどういう形で埋め込まれるのかを考えないとならない。

もちろんSchemeでCL式に書くことも、その逆もやればできなくはないけれど、 素直に有りもののライブラリを使ってゆくと、こういう異なるパラダイムの上に 乗っかってゆくことになる。

★ ★ ★

Allegro CLの方のコードも晒せると比較ができるんだけど、 出せるかどうかまだ不明なので、ちょっと抽象的な例をば。

CLの方でネストしたリストをXML化する。 (classとか適当につけてる。^@net-xml-geneartor が定義するリーダマクロ)。

(defun emit (obj port)
  (with-xml-generation (port)
    (emit-element obj)))

(defun emit-element (obj)
  (if (listp obj)
      (emit-list obj)
      @obj))

(defun emit-list (lis)
  ^((div @class "list")
    ^(ul (loop for e in lis
               for n from 0
               do (emit-item e n)))))

(defun emit-item (e n)
  ^((li @class (if (zerop (mod n 2)) "li0" "li1"))
    (emit-element e)))

出力例。

> (emit '(a b (c d) e) *standard-output*)

<div class="list">
  <ul>
    <li class="li0">a</li>
    <li class="li1">b</li>
    <li class="li0">
      <div class="list">
        <ul><li class="li0">c</li><li class="li1">d</li></ul>
      </div>
    </li>
    <li class="li1">e</li>
  </ul>
</div>
nil

同じ機能をSchemeで実現しようとすると、各下請け関数は SXMLを組み立てて返すようにしといて、一番上で完全なSXMLを受け取って レンダリングするように書くだろう。

(define (emit obj port)
  (srl:sxml->xml (build-element obj) port))

(define (build-element obj)
  (if (list? obj)
    (build-list obj)
    (x->string obj)))

(define (build-list lis)
  `(div (@ (class "list"))
    (ul ,@(map-with-index build-item lis))))

(define (build-item n e)
  `(li (@ (class ,(if (zero? (modulo n 2)) "li0" "li1")))
       ,(build-element e)))

出力:

gosh> (emit '(a b (c d) e) (current-output-port))
<div class="list">
  <ul>
    <li class="li0">a</li>
    <li class="li1">b</li>
    <li class="li0">
      <div class="list">
        <ul>
          <li class="li0">c</li>
          <li class="li1">d</li>
        </ul>
      </div>
    </li>
    <li class="li1">e</li>
  </ul>
</div>#<undef>

どちらもコードの構造はほとんど同じだから、LispにもSchemeにも詳しくない人が 見たらほとんど見分けがつかないんじゃないか。

でもある程度片方がわかる人が、もう片方をよく理解しようとすると「あれ?」となるんじゃないかな。 特に、Scheme脳の人は上のCLのコードを見た時に、

^(tag ...)<tag>...</tag>を表すのか。 ってことは^(tag ...) 式は要素を表すオブジェクトを返すのかな?」

って考えてしまうんじゃないかと思う。 実は ^(tag ...) は「コマンド」であって、値は返さない。 基本的なところで、かなり大きなパラダイムの差がある。

個人的にはScheme式の方がストレートで好きなんだけれど。 性能の問題を脇におくとしても、CL方式にはもうひとつ、レンダリングの途中で スペシャル変数をいじることでその枝だけ出力を変える、なんてことができる。 Scheme式の場合は、レンダリングが別フェーズなので、一部だけ出力を変えたければ その情報を木に埋め込むか、 あるいはモナドを使って暗黙のコンテキスト情報を受け渡して行くことになる (これはKahuaでやったんだけれど、静的型でないSchemeでモナドを使いまくるのは わりとしんどい。)

というわけでどっちも捨てがたいんだよなあ。

Tags: Programming, Lisp, Scheme, Gauche

Post a comment

Name: