ラベル Common Lisp の投稿を表示しています。 すべての投稿を表示
ラベル Common Lisp の投稿を表示しています。 すべての投稿を表示

2012-09-25

percent-encoding

Qiitaに投稿したパーセントエンコーディングについてのTipsにも書いたんだけど、自分の知ってるライブラリはどうも帯に短し襷に長しといった感じで、

  • application/x-www-form-urlencodedも扱える(似ているものなので、同じライブラリで扱いたい)
  • ASCIIに収録されていない文字を任意のエンコーディングでオクテット列に変換し、パーセントエンコーディングできる(今なら大体UTF-8で済みそうだけど、できれば他のエンコーディングも扱いたい)

という条件のものは見つからず、色々試行錯誤していた。

そんな折に@snmstsさんが教えてくださったのがdo-urlencodeで、普通のパーセントエンコーディングもapplication/x-www-form-urlencodedも両方扱えるし、UTF-8決め打ちとはいえ自分はUTF-8くらいしか使わないだろうから良いか、ということで使ってみた。

それで、使ってみて気付いたんだけど、どうもメモリの消費がちょっと多い。そしてちょっと遅い。

(let ((str (map 'string (lambda (_)
                          (declare (ignore _))
                          (code-char (random 255)))
                (make-string (* 1024 1024)))))
  (null (time (puri::encode-escaped-encoding str puri::*reserved-characters* t)))
  (null (time (urlencode:urlencode str))))

;; CCL 1.8
;; (PURI::ENCODE-ESCAPED-ENCODING STR PURI::*RESERVED-CHARACTERS* T)
;; took 82,384 microseconds (0.082384 seconds) to run.
;; 13,839 microseconds (0.013839 seconds, 16.80%) of which was spent in GC.
;; During that period, and with 2 available CPU cores,
;; 64,990 microseconds (0.064990 seconds) were spent in user mode
;; 13,998 microseconds (0.013998 seconds) were spent in system mode
;; 18,647,856 bytes of memory allocated.
;; 40 minor page faults, 0 major page faults, 0 swaps.
;; (URLENCODE STR)
;; took 7,272,771 microseconds (7.272771 seconds) to run.
;; 1,337,318 microseconds (1.337318 seconds, 18.39%) of which was spent in GC.
;; During that period, and with 2 available CPU cores,
;; 7,079,924 microseconds (7.079924 seconds) were spent in user mode
;; 171,974 microseconds (0.171974 seconds) were spent in system mode
;; 1,178,938,384 bytes of memory allocated.
;; 264 minor page faults, 0 major page faults, 0 swaps.

;; SBCL 1.0.58
;; Evaluation took:
;; 0.053 seconds of real time
;; 0.052993 seconds of total run time (0.046993 user, 0.006000 system)
;; 100.00% CPU
;; 124,622,974 processor cycles
;; 18,647,216 bytes consed
;;
;; Evaluation took:
;; 3.412 seconds of real time
;; 3.395483 seconds of total run time (3.268503 user, 0.126980 system)
;; [ Run times consist of 0.404 seconds GC time, and 2.992 seconds non-GC time. ]
;; 99.50% CPU
;; 7,962,815,308 processor cycles
;; 770,256,208 bytes consed

これはpuriのエンコーダと比較した結果なんだけど、4MB分の文字列を扱うのにCCLで1GB超、SBCLで700MB超のメモリを使っている。ワーオ。

コードを読んだところ、どうもエスケープ一回ごとに文字列とベクタのアロケーションが起きるようになっていた。ここを何とかすればメモリ消費量や処理速度が改善できるんじゃないだろうかうへへ、ということで改造することに。

で、試行錯誤の結果、大幅にメモリ消費量と速度は改善した。したんだけど、どうも中身が完全に別物になってしまった。総書き換えといった感じで、これを作者に取り込むように主張しても、「お前これ同じなの関数のインターフェイスだけやん」と言われる未来が容易に想像できたので、仕方なく別のライブラリに仕立てることにした。「自分で書いてしまったりしがち」とかQiitaのTipsに書いておいてこれだから割と救いようがない。

そうと決めたらもう開き直って、UTF-8以外のエンコーディングのサポートとか、puriで見かけて良いと思った、どの文字をエスケープするか指定できる機能とかも付けてみた。その結果がこれGitHubのミラーもある。

普通に使う分には難しいことはなく、

(percent:encode str)

とすればエンコードできるし、

(percent:decode str)

とすればデコードできる。エンコードやデコードする文字を指定する場合はこんな感じ。

;; 特定の文字だけエンコードしたり
(percent:encode "/usr/bin/["
                :test (lambda (x)
                        (or (percent:unreservedp x)
                            (= x #x2f))))    ; '/'
;=> "/usr/bin/%5B"

;; 特定の文字だけデコードできる
(percent:decode (percent:encode "a:b/c" :test (constantly nil))
                :test #'percent:alphap)
;=> "a%3Ab%2Fc"

こんなことできて何が嬉しいの? という風に思うかもしれないけど、URIはそれぞれのコンポーネントで許される文字が違うので、そういうのに対応するときに便利。あとはする必要がない文字までエンコードしてるURIの正規化とか、規格に沿わない実装に対応するのにも使える。詳しい使い方はREADMEやユニットテストを参照のこと。

あんまり需要はないと思うけど、自分で使うために書いたので問題ない。将来的にもっと良さげなライブラリを見つけたらそっちを使うかも。というか、誰か書いてください。

2012-09-09

続々sequentially-apply

ネタはさておき、実際のコードで使ってみた例とかも載せておく。

;; before
(defun sha1 (obj)
  (let* ((string (encode-json-to-string obj))
         (octets (string-to-octets string))
         (digest (digest-sequence :sha1 octets)))
    (byte-array-to-hex-string digest)))

;; after
(defun sha1 (obj)
  (sequentially-apply (encode-json-to-string obj)
    (string-to-octets _)
    (digest-sequence :sha1 _)
    (byte-array-to-hex-string _)))

誤差だと思えてならないが、相対的にすっきりはしている。気になるのは、コードを読むときに視点が右上から左下に動くこと。微妙に読み辛い?

(defun sha1 (obj)
  (pipe (encode-json-to-string obj)
        (string-to-octets _)
        (digest-sequence :sha1 _)
        (byte-array-to-hex-string _)))

みたいに揃ってる方が、視点が上から下って感じで見易い気もする。

2012-09-08

cl-fn 0.2

自分自身でもすっかり投げっぱなしで忘れていた関数ユーティリティライブラリ、cl-fnの0.2をリリースした。

  • sequentially-apply
  • ->
  • ->>

というマクロが新たに追加されている。

インストールするには、

% cd ~/quicklisp/local-projects
% wget -q -O - https://bitbucket.org/llibra/cl-fn/downloads/cl-fn-0.2.tar.bz2 | bzcat | tar xf -

みたいな感じで。普通にブラウザとかでダウンロードしてきても、最終的にQuicklispのlocal-projectsディレクトリの下でアーカイブを展開すれば大丈夫。もちろん、Mercurial使ってる人はhg cloneでもオーケー。Git使ってる人はGitHubのリポジトリがあるのでどうぞ。自分以外に使う人がいるとも思えないけど一応。

quicklisp/local-projectsの下に展開し終わったら、

> (ql:quickload :cl-fn)

すればcl-fnの関数やマクロが使えるようになる。パッケージ名はそのまんまcl-fn。

で、本題。新しく導入したマクロの話。

->->>Clojureから借りてきたもの。使い方はリンク先に載っているので読んでほしい。日本語の解説を読みたい方は「Clojureの->と->>の使い方 - あと味」辺りが詳しいのでどうぞ。簡単に説明すると、関数の戻り値を順番に適用していきたい場合に便利なマクロ。->は最初の引数、->>は最後の引数として戻り値を渡す。

sequentially-apply->->>の亜種みたいなものだけど、こちらはprogsを参考にして作った。オリジナルとの違いは、戻り値を渡す位置を「_」で指定できるようにしているのと、多値に対応していること。こんな風に使う。

(sequentially-apply (loop for n from 1 to 10 collect n)
  (reduce #'+ _)
  (print _))
;-> 55

ひとつ前の式の戻り値を使いたい場所に「_」を指定しておくと、戻り値に置き換わる感じ。

多値を使う場合はこうする。

(sequentially-apply (values 1 2 3)
  (mapcar (lambda (x) (* x 2)) (list _ _ _))
  (reduce #'+ _))
;=> 12

それぞれの値が順番に「_」の部分に展開されると考えて問題ない。ちなみに、順番の入れ替えは現状ではできないので注意。入れ替えたい場合は

(sequentially-apply (values 1 2 3)
  (let ((x _) (y _) (z _)) (list z y x)))
;=> (3 2 1)

(sequentially-apply (values 1 2 3)
  ((lambda (x y z) (list z y x)) _ _ _))
;=> (3 2 1)

のようにletlambdaを経由することになる。

ついでに、sequentially-applyについて考えたこと、考えていることとかも書いておく。

引数を「_」で指定することについては、タイプ数が少し増えるけど、->->>みたいに複数のマクロを作る必要がないこと、それぞれの式が本来の呼び出しの形に似ていることからこうしてある。その辺りが好みじゃなければ->->>を使ってくださいというスタンス。

多値の順番の入れ替えについては、「_1」「_2」みたいに順番を指定する、みたいなことは割と簡単にできるけど、どうなんだろう。自分的にはそんなにない状況な気がするし、letで良いんじゃ、とか思うけど、良くわからない。不便だと思ったらいつか対応するかも。ただ、「_1」と「_」が混ざったらどうするの、とか考えると割と微妙かもしれない。

名前については、良い感じの短い名前が思い付かなかった。個人的に記号だけの識別子は好きじゃないから避けたいけど、pipeとかも微妙に感じるし……。将来的に変えるかもしれない。記号が駄目なら->とかはどうなんだ、という話だけど、そっちも良い名前が思い付いたら変えるかも。ただ、元の->とかも別名として残すとは思う。逆にsequentially-applyの方も、長いから「>>」っていう別名付けてよ、みたいに誰か他の人が言ってきたら多分対応する。まあ、まずあり得ないからこっちは気にしない。

こんなところかな。lambdaの代わりのfn(「_」で引数無視できて楽)とか、定番のcurryrcurrycomposeconjoindisjoinflipとか、(setf (symbol-function ...) ...)の代わりのdefaliasとか、あとは今回の->->>sequentially-applyとか、そういうのを求めてる人は気が向いたらどうぞ。

2012-08-23

パッケージに対する雑感

よく他の言語で、コードを読みにくくするので、特定の名前空間を丸ごと取り込む(OCamlのopen MとかHaskellのimport MとかC++のusing Nとか)のは要注意って言われるけど、Common Lispの場合どれだけuse-packageしても、

(symbol-package 'x)

ホームパッケージを確認できるから、気軽にシンボルインポートできる。これは、識別子(シンボル)がファーストクラスオブジェクトなことによる大きな利点だと思う。運用で回避したり(名前空間を取り込む代わりに、短い別名を付けてアクセスすることが多い)、ツールで補助する必要があまりない。

また、名前空間(パッケージ)自体もファーストクラスなオブジェクトなので、どういうシンボルが含まれるかとか、他から取り込んでいるシンボルはどれかとか、そういうことをコードの字面から読み取る情報としてではなく、計算可能なデータとして扱うことができる。これも大きなアドバンテージじゃないだろうか。Lispの動的、対話的に開発する文化が良く出ている機能だとも思う。

対して、少し扱いづらいと感じる部分もある。例えば、OCamlにはlocal modulelocal openという機能があって、

(* ListモジュールにLという別名を付ける *)
let module L = List in L.iter print_string [ "a"; "b"; "c" ]

(* 部分的にListモジュールをopenする *)
let open List in iter print_string [ "a"; "b"; "c" ]

のようなことができるけど、こういう粒度の細かい名前空間の切り替えは、リーダーとパッケージの仕様の都合上、ANSI Common Lispの範囲ではできないと思う。(リーダーマクロを使えば実現できそう)

ちなみに、部分的にuse-packageするのとは少し違うけど、Allegro CLSBCL(1.0.55以降)には、

ppcre::(split ":" "a:b:c")

のように、指定したパッケージをカレントパッケージにして式を評価する構文がある。ただし、式の中ではパッケージが切り替わっているので、

cl-user::(let ((x 0)) cl::(print x))    ; cl::(...)の中のxの参照はcl:xの参照と解釈される

こういうことはできない。

他にも、シンボルが衝突するなどの理由でパッケージをuse-packageしたくない、でもこの長いパッケージ名を毎回明示するのは厳しい、という場合、

(let ((nicknames (package-nicknames :too-long-package-name)))
  (rename-package :too-long-package-name
                  :too-long-package-name
                  (cons :short-name nicknames)))

なんて感じで、新しい別名を付けたりすると思う。書いているコードがアプリケーションなら特に問題はないんだけど、それがライブラリだったとき困る。ライブラリをロードするたびに特定のパッケージに別名を付けてしまうのは、いかにも行儀が悪い気がするし、その別名が他のパッケージ名と衝突なんてした暁には、ライブラリの利用者に対処してもらうことになって色々悲しい。

(defparameter *old-nicknames* (package-nicknames :too-long-package-name))

(rename-package :too-long-package-name
                :too-long-package-name
                (cons :short-name *old-nicknames*))

...

(rename-package :too-long-package-name
                :too-long-package-name
                *old-nicknames*)

のようにすることはできるけど、これを毎回書くのもどうなんだという気もする。この辺りの事情を改善するためのものがFrancois-Rene Rideauのpackage-renamingだと思うんだけど、READMEを読む限りでは、少しトリッキーな感じで他の人には勧めにくい。

とまあ、物足りなく感じる部分もあるけど、Common Lispのパッケージがとても柔軟で強力な仕組みなのは間違いないと思う。特に、対話的に開発する場合には威力を発揮する。……と、他の言語を使っていて改めて実感した次第。

2011-08-31

16進ダンプ

REPLから使いたくなって書いた。

使い方は以下の通り。(unsigned-byte 8)を要素に持つシーケンスを扱える。十行ごとにヘッダが入り、アドレス表示部分は長さ(標準で8桁)とオフセットを指定できる。テキスト表示部分はstandard characterかつgraphicな文字だけ表示できる。

> (hex-dump (sb-ext:string-to-octets "string"))
========== +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +A +B +C +D +E +F =================
00000000 : 73 74 72 69 6E 67                               | string
NIL
>

2011-08-05

cl-fn

昨日のdefaliasだけど、似たようなコードを発見した。

Ron Garretユーティリティライブラリで、

(defmacro define-synonym (s1 s2)
  `(progn
     (defun ,s1 (&rest args) (declare (ignore args)))
     (setf (symbol-function ',s1) (function ,s2))))

という関数を定義してた。

何度か目を通してるはずなのに何故気付かなかったんだ、と一瞬後悔したけど、良く見たらs2を直接function特殊形式に渡してる。これだと、関数名lambda式以外を渡すことが許されないので、純粋に別名を付けるためだけを目的としたもののようだ。セーフ。こちらの主な目的は関数合成とかの結果に名前を付けることだから、上のコードは使えない。

安心した所で、無駄にならなかったdefaliasを、関数を扱うときに良く使われるユーティリティ関数とまとめてcl-fnというライブラリにした。ついでに、lambdaを書くのに疲れてきたので、前述のRon Garretのユーティリティからfnというマクロも貰ってきた。

;; &restを使わずに引数全体を束縛できる
(funcall (fn args args) 0 1 2)
;=> (0 1 2)

;; _で引数を無視できる(警告が出ないように宣言も付く)
(funcall (fn (x _) x) 0 1)
;=> 0

なんてことができるちょっと賢いやつ。ただし、リーダには手を入れないので、

((fn (x) x) 0)

みたいなことはできない。直接lambda式を呼びたいことなんてあんまりないから問題ないとは思うけど。

ちなみに、使いたい機能だけ楽にインポートできるように、パッケージを細かく分けておいた。例えば、curryとrcurryだけ使いたい場合は、cl-fn.paパッケージだけ使えば大丈夫。全部入りが欲しいならcl-fnを使う。余分なものまでインポートしなくて良いので綺麗好きな人でも安心。

2011-08-04

defalias

Emacs Lispには、関数に別名を付けるdefaliasという関数があるんだけど、これが欲しくなったので書いてみた。

(defmacro defalias (name function-designator)
  (with-gensyms (function designator)
    `(let* ((,designator ,function-designator)
            (,function (if (functionp ,designator)
                           ,designator
                           (symbol-function ,designator))))
       (setf (symbol-function ',name) ,function)
       ',name)))

マクロにしたのはクォートを付けるのが面倒だから。第二引数はfunction designatorなので、シンボルでも関数でも大丈夫。

普通に別名を付けることもできるけど、考えている主な使い所は、合成したり部分適用した関数に名前を付けたりするとき。

;; これを
(setf (symbol-function '2*)
      (curry #'* 2))

;; こう書ける
(defalias 2* (curry #'* 2))

(2* 3)  ;=> 6

他にも誰かが似たようなものを作ってそうだったから探してみたんだけど、見付けられなかった。

2011-07-06

string=の意外な利点

今まで、string=を使わずにequalで済ませていた。性能が要求される場面ではstring=の方が良いと思うけど(equalは汎用の比較関数なので、処理の振り分けの分だけどうしても遅くなる。ただ、型宣言をすれば、処理系によってはstring=と同じ処理に最適化されるかもしれない)、そんなコードなんて全然書かない。なので、すっかり存在を忘れていた今日この頃。Ron Garretユーティリティを読んでいたら、自分にとっては意外な利点を発見した。string=は文字列とシンボルの比較に便利。

ユーティリティの中にある、

(defun ignorevar? (v)
  (and (symbolp v) (string= v "_")))

というコードを読んで気付いたんだけど、string=に直接シンボルを渡している。あれ? と思ってHyperSpecを引いたら、渡すのはstring designatorで良いらしい。string designatorっていうのは、文字とシンボルと文字列のこと。

以前書いたマクロの中で、

(defun <-p (x)
  (equal (symbol-name x) "<-"))

っていうコードがあったんだけど、string=を使えば、

(defun <-p (x)
  (string= x "<-"))

こう、ちょっとだけ短く書けて、ちょっとだけ嬉しい。大きな違いはないんだけど、ささやかな幸せ、というか。

何にせよ、勉強になった。

2011-06-28

パターンマッチングによる処理の分岐(2):Fare-matcher

Fare-matcherは、MLスタイルのパターンマッチングによる処理の分岐を、本格的にCommon Lispに取り入れるために作られたライブラリ。リストと多値、ベクタ、CLOSオブジェクトに対応し、パターンの表現力が高い。以下は2011年6月28日現在で最新のリビジョンの解説。

まずはパターンについて。パターンはS式で表現され、次のような種類がある。なお、定義リスト中の「*」と「+」は、0回以上の繰り返し、1回以上の繰り返しを表す。

リテラル
値同士をequalで比較して一致するときにマッチする
シンボル
どんな値にもマッチし、その値でシンボルを束縛する。ただし「_」と「*」の場合、値を束縛せずに捨てる(ワイルドカード)
(cons パターン パターン)
consは、car部とcdr部がそれぞれのパターンとマッチするコンスにマッチする
(list パターン+)
listは、要素の数がパターンの数と一致し、それぞれの要素の値とパターンがマッチするリストにマッチする
(list* パターン* シンボル)
list*は、要素の数がパターンの数以下で、それぞれの要素の値とパターンがマッチするリストにマッチする。最後のシンボルは、パターンのマッチングが済んだ後に残っている要素のリストで束縛される
(values パターン+)
valuesは、それぞれの値とパターンがマッチする多値にマッチする。多値の数とパターンの数が一致しない場合、nilがマッチングに使われる
(vector パターン+)
vectorは、要素の数がパターンの数と一致し、それぞれの要素の値とパターンがマッチするベクタ(一次元の配列)にマッチする
(slot* (スロット名 パターン)+)
slot*は、スロットの値とパターンがマッチするCLOSオブジェクトにマッチする
(accessor* (アクセサ名 パターン)+)
accessor*は、アクセサが返す値とパターンがマッチするCLOSオブジェクトにマッチする
(and パターン+)
andは、値とすべてのパターンがマッチしたときにマッチする
(or パターン+)
orは、値とパターンのいずれかがマッチしたときにマッチする
(of-type 型)
of-typeは、値の型と型が一致したときにマッチする
(when テスト)
whenは、テストが真のときにマッチする(ガード)
(like-when パターン テスト)
like-whenは、値とパターンがマッチし、テストが真のときにマッチする。(and パターン (when テスト))の構文糖

whenやlike-when、andやorの組み合わせで、複雑なパターンを作ることができる。

次はAPIについて。

ifmatch pattern exp &optional (ifmatches t) iffails
ifm pattern exp &optional (ifmatches t) iffails
ifと同じ二方向の条件分岐。expがpatternにマッチした場合ifmatchesを評価し、しなかった場合iffailsを評価する。ifmはifmatchの別名。
match exp &rest clauses
ematch exp &rest clauses
condやcaseなどと同じ、任意の数の方向への条件分岐。clauseのcar部がパターンで、cdr部がマッチした場合の処理。caseとecaseの関係と同じで、ematchはどのパターンにもマッチしない場合、エラーを発生させる。
letm pattern val &body body
destructuring-bindのような、構造化代入のためのマクロ。valがpatternにマッチした場合bodyを評価し、しなかった場合はエラーを発生させる。

letmという名前はモナド方面で使われている可能性があるため、use-packageするときは気を付けたほうが良いかもしれない。

使用例を挙げる。

(ql:quickload :fare-matcher)
(defpackage :fare-matcher-test (:use :cl :fare-matcher))
(in-package :fare-matcher-test)

(ifm 0 0)
(ifm 'x 'x)
(ifm #\a #\a)
(ifm "a" "a")
;=> T

(ifm x 2 x)
;=> 2

(ifm (list x y) '(0 1) (values x y))
;=> 0, 1

(ifm (cons f r) '(0 1 2) (values f r))
(ifm (list* f r) '(0 1 2) (values f r))
;=> 0, (1 2)

(ifm (values x y) (truncate 3.14) (values x y))
;=> 3, 0.1400001

(ifm (vector 1 2 x) #(1 2 3) x)
;=> 3

(defclass point ()
  ((x :accessor point-x :initarg :x :initform 0)
   (y :accessor point-y :initarg :y :initform 0)))

(let ((p (make-instance 'point :x 3 :y 2)))
  (ifm (slot* (x px) (y py)) p (values px py)))
;=> 3, 2

(let ((p (make-instance 'point :x 4 :y 5)))
  (ifm (accessor* (point-x px) (point-y py)) p (values px py)))
;=> 4, 5

(ifm (and (cons x 2) (cons 1 y)) '(1 . 2) (values x y))
;=> 1, 2

(ifm (or 1 3) 3)
;=> T

(ifm (of-type number) 1)
;=> T

(ifm (when t) :anytime-t)
(ifm (and x (when (symbolp x))) 'symbol)
;=> T

(ifm (like-when (list x y z) (eql (+ x y z) 6)) '(1 2 3))
(ifm (like-when (list x y z) (eql (+ x y z) 6)) '(0 0 6))
;=> T

(ifm (list (list x y) z) '((:a :b) :c) (values x y z))
;=> :A, :B, :C

(ifm (cons (cons key value) rest) '((:x . 100) (:y . 200)) (values key value rest))
;=> :X, 100, ((:Y . 200))

複雑な構造のリストも難なく処理できるため、リスト処理に威力を発揮する。

マクロを展開してみる。

; (ifm (cons f r) '(0 1 2) (values f r))
(FLET ((#:FAILURE-CONTINUATION NIL NIL))
  (DECLARE (IGNORABLE #'#:FAILURE-CONTINUATION))
  (LET ((#:FORM
         (MULTIPLE-VALUE-BIND (#:R012110)
             '(0 1 2)
           #'(LAMBDA NIL (VALUES #:R012110))))
        F
        R)
    (DECLARE (IGNORABLE #:FORM))
    (FUNCALL (BLOCK #:M%POINT
               (MULTIPLE-VALUE-CALL
                 (FUNCALL #'(LAMBDA (#:R012110 #:R112111)
                              #'(LAMBDA (#:FORM)
                                  (FARE-UTILS:MVBIND (#:R212112 #:R312113)
                                                     (FUNCALL
                                                      #'(LAMBDA
                                                         (FARE-MATCHER::FORM)
                                                         (IF (CONSP FARE-MATCHER::FORM)
                                                             (PROGN (VALUES (CAR FARE-MATCHER::FORM) (CDR FARE-MATCHER::FORM)))
                                                             (RETURN-FROM #:M%POINT #'#:FAILURE-CONTINUATION)))
                                                      #:FORM)
                                                     (PROGN (FUNCALL #:R012110 #:R212112) (FUNCALL #:R112111 #:R312113)))))
                          #'(LAMBDA (#:FORM) (SETF F #:FORM))
                          #'(LAMBDA (#:FORM) (SETF R #:FORM)))
                 (FUNCALL #:FORM))
               #'(LAMBDA NIL (VALUES F R))))))

funcallでクロージャを呼ぶようになっている。このため、On Lispの「構造化代入」で紹介されているような、マクロでifなどのプリミティブな式まで変換する手法に比べて、コストが大きい。

(declaim (optimize (speed 3) (safety 1) (debug 1) (space 1)))

(time (iter (repeat 1000000)
            (letm (list x y z)
                (list (random 10) (random 10) (random 10))
              (values x y z))))
;-> (ITER (REPEAT 1000000) (LETM (LIST X Y Z) (LIST (RANDOM 10) (RANDOM 10) (RANDOM 10)) (VALUES X Y Z))) took 1,906 milliseconds (1.906 seconds) to run 
;                       with 2 available CPU cores.
;   During that period, 1,766 milliseconds (1.766 seconds) were spent in user mode
;                       47 milliseconds (0.047 seconds) were spent in system mode
;   531 milliseconds (0.531 seconds) was spent in GC.
;    352,000,032 bytes of memory allocated.

(time (iter (repeat 1000000)
            (destructuring-bind (x y z)
                (list (random 10) (random 10) (random 10))
              (values x y z))))
;-> (ITER (REPEAT 1000000) (DESTRUCTURING-BIND (X Y Z) (LIST (RANDOM 10) (RANDOM 10) (RANDOM 10)) (VALUES X Y Z))) took 312 milliseconds (0.312 seconds) to run 
;                       with 2 available CPU cores.
;   During that period, 312 milliseconds (0.312 seconds) were spent in user mode
;                       0 milliseconds (0.000 seconds) were spent in system mode
;   63 milliseconds (0.063 seconds) was spent in GC.
;    24,000,032 bytes of memory allocated.

速度を気にしない場所では問題にならないけど、性能が必要な部分で使うには厳しいかもしれない。とは言え、いつでも速度が求められるわけではないから、例えば、複雑なリスト処理が必要とされがちだけど、速度は問題にならないマクロの定義などでは、便利に使えるんじゃないだろうか。少なくとも自分は便利に使っている。

まとめると、性能が要求される部分ではボトルネックになる可能性があるが、複数のデータ型に対応し、表現力も高いため、速度を気にしないで良い部分では威力を発揮する。

以下参考文献。

2011-06-25

パターンマッチングによる処理の分岐(1):Alexandria

OCamlHaskellなどのコードを読むと、パターンマッチングによる処理の分岐が良く出て来る。この機能は実に便利で、リストの要素の内容に合わせて処理を分岐したり、関数の引数の数で処理を分岐したり、といった処理を、非常に簡潔に、そして読みやすく書ける。自分はこれをAndrew Wrightが書いたSchemeのマクロで知ったんだけど(Schemeの世界では有名なマクロ)、今では手放せなくなってしまった。今回は、パターンマッチングによる処理の分岐をCommon Lispで使うとき、どうすれば良いかの案内。パターンマッチングによる処理の分岐が、具体的にどのように便利かは、OCamlやHaskellなどの関数型言語の文書で多く紹介されているので、そちらを読んで欲しい。On Lispの「構造化代入」の章を読むのも良い。

パターンマッチング自体は特に新しい技術ではないので、Lispの世界でも馴染みが深い。On Lispにも登場するし、読んでないけどPAIPでも触れられているそうだ。Makoto Hiroiさんのxyzzy Lisp Programmingでも、「記号のパターンマッチング(1)」で取り上げられている。こういったコードを持ってきても良いんだけど(ライセンスに注意。幸いどれも自由に使えるけれど、使ったコードを第三者に公開するなら、出典は明記しなければいけないだろう)、使う側からすると、最初からASDFに対応していて欲しいし、Quicklispで楽に読み込めるともっと嬉しい。

そして、その嬉しいことに、パターンマッチングによる処理の分岐に使えるライブラリが、2011年6月25日の時点でもQuicklispに登録されている。自分が知っている範囲では、

  • Alexandria
  • Fare-matcher
  • arnesi
  • cl-match
  • cl-pattern

がそうだ。一連の記事で順番に紹介していく。

Alexandriaは、Common Lispを今でも使っている人ならお馴染みのユーティリティライブラリだ。with-gensymsやonce-only、flattenなどの、有名なユーティリティマクロや関数が収録されている。その中のdestructuring-caseで、非常に限定された形だけれど、パターンマッチングによる処理の分岐ができる。

(alexandria:destructuring-case '(3 0 1 2)
  ((1 x) x)
  ((2 x y) (+ x y))
  ((3 x y z) (+ x y z)))
;=> 3

(alexandria:destructuring-case '(:a :b :c)
  ((x y z) :in-destructuring-case)
  ((:a x y) :car-cannot-take-binding))
;=> :CAR-CANNOT-TAKE-BINDING

(alexandria:destructuring-case '(1 #\x "string")
  ((1 #\x "string") :literal-causes-error))
;>> Error: Invalid lambda list: (#\x "string")

第一引数がパターンマッチングの対象になるリストで、以降がパターンと対応する処理の組み合わせだ。このマクロは名前の通り、destructuring-bindとcaseを組み合わせたもので、それぞれの性質を引き継ぐ。展開したあとの式も見てみよう。

(LET ((#:KEY0 '(3 0 1 2)))
  (IF (TYPEP #:KEY0 'CONS)
      (CASE (CAR #:KEY0)
        (1 (DESTRUCTURING-BIND (X) (CDR #:KEY0) X))
        (2 (DESTRUCTURING-BIND (X Y) (CDR #:KEY0) (+ X Y)))
        (3 (DESTRUCTURING-BIND (X Y Z) (CDR #:KEY0) (+ X Y Z))))
      (ERROR "Invalid key to DESTRUCTURING-~S: ~S" 'CASE #:KEY0)))

とても単純な仕組みになっている。パターンのcar部はcaseで処理できなければならない。パターンのcdr部はdestructuring-bindに渡されるため、リテラルを含んではいけない。

一見、かなり厳しい制限に思えるけど、リストの最初の要素が処理の成否を表し、それ以降の要素が処理のデータを表す、といった、ありがちなパターンには十分対応できる。あるいは、最初の要素はアクションを表し、以降の要素はアクションのパラメータを表す、なんていう場合でも問題ない。リストの最初の要素にはアクセスしやすいので(carするだけだ)、リストの最初に条件分岐のためのキーを置くことは多い。多値にもmultiple-value-listで対応でき、意外に適用範囲は広い。

(alexandria:destructuring-case '(nil (10 . 5) "syntax error")
  (((t) syntactic-tree) (process syntactic-tree))
  (((nil) (line . char) msg) (error "line ~a: char ~a: ~a" line char msg)))
   
(alexandria:destructuring-case `(:write ,stream #\a)
  ((:write s x) (princ x s))
  ((:read s) (aif (read-char s nil) (push it buf))))

特徴をまとめると、alexandria:destructuring-caseは、対応するデータ型はリストのみで、表現力も乏しいけれど、限られた用途には十分で、仕組みが単純なため、destructuring-bindが最適化されている処理系では速い。

2011-02-01

Clozure CLの最適化について

書きかけ:多分、加筆・訂正します。

Clozure CLは、生成するコードの最適化を、ポリシーオブジェクトというものを使って管理している。対話的なコンパイルに使われるものや、compile-fileに使われるものなど、複数のポリシーオブジェクトが存在するが、デフォルトでは全てのポリシーオブジェクトは同じ設定になっている。

ポリシーオブジェクトは構造体のようなもので、それぞれのスロットには関数が収納されている。関数は、一部の例外を除き、レキシカルな環境を引数として受け取る。イメージとしては、

#'(lambda (env)
    (< (debug-optimize-quantity env) 2))

こんな感じ。コンパイラは、これらの関数が返す真偽値を元にして、最適化を行う。つまり、ポリシーオブジェクトのスロットの数の分だけ、最適化のオプションがある。

具体的な内容は以下の通り。1.6の時点での情報。項目の名前は、ポリシーオブジェクトのスロットにアクセスするマクロの名前で、動作はデフォルトのもの。

policy.allow-tail-recursion-elimination
末尾再帰の最適化をするかどうか。debugが2未満のときに真。
policy.inhibit-register-allocation
詳細不明。debugが3のときに真。
policy.inhibit-event-checking
現状では何にも影響しない。このオプションが存在しないブランチもある。
policy.inline-self-calls
詳細不明。debugが3未満のときに真。
policy.allow-transforms
コンパイラマクロを展開するかどうか。safety、compilation-speed、debugが3未満で、speedが1以上のときに真。
policy.force-boundp-checks
詳細不明。safetyが3のときに真。
policy.allow-constant-substitution
詳細不明。つねに真。
policy.open-code-inline
極端に長いインストラクションをインライン展開するかどうか。speedがspaceより大きいとき真。コストの高いスペシャル変数の参照などがインライン展開される。
policy.inhibit-safety-checking
引数の数、型、束縛の有無などのチェックを省くかどうか。speedが3で、safetyが0のとき真。型のミスマッチがクラッシュを引き起こす可能性がある。
policy.trust-declarations
型宣言が利用されるかどうか。safetyが3未満で、speedがsafety以上のとき真。固定長整数同士の演算では型チェックが省かれ、リストへの操作は安全でないコードが生成される。それ以外の型では型特有のコードが生成されるが、policy.inhibit-safety-checkingが真でなければ、安全なコードが生成される。
policy.declarations-typecheck
型宣言が実行時に行われるかどうか。safetyが3か、speedがsafety未満のとき真。theを実行時の型チェックのコードにコンパイルし、束縛や破壊的変更の際に型チェックを行うコードが挿入される。

以下参考文献。

2011-01-26

誰でもわかるわけないコンパイラマクロ

Common Lispのソースコードを読むとたまに出てくる、define-compiler-macro。これは何ぞや、と思ったことのある人も多いのではないだろうか。今回のテーマはコンパイラマクロ。

define-compiler-macroとは、その名の通り、コンパイラマクロを定義するマクロのこと。では、コンパイラマクロとは何なのか?

端的に言えば、マクロの一種だ。普通のdefmacroで定義するマクロと何が違うかというと、

  1. 他の関数やマクロと名前が重複しても良い
  2. コンパイラマクロを展開する前の式が&whole引数に束縛される
  3. funcallによる呼び出しも展開される
  4. 展開しなくてもいい

これを見ても、何に使うのかちょっと予想できないかもしれない。他と名前が重複したら、展開するときに困るだろうし、funcall? マクロなのに? それに、展開しないようにすることができるって、展開しないマクロって何なんだ?

一見不可解なこれらの特徴も、特定の場面ではとても便利に使える。最適化だ。

具体的な例を見た方が分かりやすい。HyperSpecのdefine-compiler-macroのExamplesから引用した。

squareは単にxを二乗する関数だが、ここでは同じ名前のコンパイラマクロを定義している。このコンパイラマクロは、引数のxがアトムだった場合、(expt x 2)へと展開し、(square (square x))という式の場合、(expt x 4)へと展開する。(square (expt x y))という式は(expt x z)になる。それ以外の式は展開されない。つまり、そのまま関数squareが呼ばれる。

また、squareという関数は、コンパイラマクロとは別に存在するため、mapcarのような高階関数でもsquareは普通に使える。

コンパイラマクロが、同じ動作をする通常のマクロより優れた点はここだ。通常のマクロでもコンパイラマクロと同じようなことはできるが、通常のマクロは高階関数には渡せない。コンパイラマクロと関数の組み合わせは、利用する側のコードからは、普通の関数と全く同じように扱える。

コンパイラマクロは、非常に強力な武器になる。どれほど時間がかかる計算でも、与えられるパラメータが定数であれば、コンパイルするときに定数に置き換えてしまえる。単なるマクロだから、ボトルネックのパターンだけ、高速なコードに展開することも容易だ。

ただ、メリットに応じたデメリットもある。コンパイラマクロは人が行う手動の最適化なので、元の関数が変更されて動作が変わった場合、コンパイラマクロも同じ動作をするように、人が手動で変更しなくてはならない。保守の手間が増えるため、度が過ぎた濫用、早過ぎる最適化は、自分の首を絞めかねない。気を付ける必要がある。

普段はあまり日の目を見ないコンパイラマクロ。性能上のボトルネックに悩んでいる場合、ひとつの選択肢として検討してみて欲しい。

以下参考文献。

2010-12-29

文字列操作の性能(1)

Common Lispで文字列を操作する場合に、何種類かある方法のうち、どれが一番効率的か知りたかったので、調べることにした。今回は参照編。

次のコードを使って、シーケンシャルアクセスの時間を測った。処理系はClozure CL 1.6。get-internal-real-timeの単位はミリ秒。

(defmacro with-random-string ((var &optional (length 1000)) &body body)
  `(let ((,var (make-string ,length)))
     (iter (for i index-of-string ,var)
           (setf (aref ,var i) (code-char (+ 32 (random 95)))))
     ,@body))

(defmacro with-null-stream ((var) &body body)
  `(let ((,var (make-two-way-stream (make-concatenated-stream) 
                                    (make-broadcast-stream))))
     (with-open-stream (,var ,var)
       ,@body)))

(defmacro with-benchmark (&body body)
  (with-gensyms (time)
    `(let ((,time (get-internal-real-time)))
       ,@body
       (- (get-internal-real-time) ,time))))

(defparameter *times* 1000)
(defparameter *length* 1024)

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (char str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (aref str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-benchmark
                 (iter (for i index-of-string str)
                       (princ (elt str i))))))))

(iter (repeat *times*)
      (sum (with-random-string (str *length*)
             (with-null-stream (*standard-output*)
               (with-input-from-string (s str)
                 (with-benchmark
                   (iter (for c = (read-char s nil))
                         (while c)
                         (princ c))))))))

結果は、

関数 所要時間(ミリ秒)
char 1093
aref 998
elt 1063
read-char 1189

arefが一番速い。eltは、型に合わせて下位のアクセサを呼ぶと思っていたので、一番速くないのは予想通り。何となくcharはプリミティブなアクセサだと思い込んでいたけど、実際はarefとeltよりも遅い。文字列ストリームが一番遅いのは予想外。予想以上に上位の関数で組み立てられている模様。

実際のコードを読んでみた。まずはchar。

(defun char (string index)
  "Given a string and a non-negative integer index less than the length of
  the string, returns the character object representing the character at
  that position in the string."
  (if (typep string 'simple-string)
    (schar (the simple-string string) index)
    (if (stringp string)
      (multiple-value-bind (data offset) (array-data-and-offset string)
        (schar (the simple-string data) (+ index offset)))
      (report-bad-arg string 'string))))

scharを呼んでいる。scharの定義は、

(defun schar (s i)
  "SCHAR returns the character object at an indexed position in a string
   just as CHAR does, except the string must be a simple-string."
  (let* ((typecode (typecode s)))
    (declare (fixnum typecode))
    (if (= typecode target::subtag-simple-base-string)
      (aref (the simple-string s) i)
      (report-bad-arg s 'simple-string))))

で、結局呼ぶのはaref。Common Lispでは文字列は文字のベクタなので、ベクタに対する処理のarefの方が下位ということなんだろう。

(defun aref (a &lexpr subs)
  "Return the element of the ARRAY specified by the SUBSCRIPTS."
  (let* ((n (%lexpr-count subs)))
    (declare (fixnum n))
    (if (= n 1)
      (%aref1 a (%lexpr-ref subs n 0))
      (if (= n 2)
        (%aref2 a (%lexpr-ref subs n 0) (%lexpr-ref subs n 1))
        (if (= n 3)
          (%aref3 a (%lexpr-ref subs n 0) (%lexpr-ref subs n 1) (%lexpr-ref subs n 2))
          (let* ((typecode (typecode a)))
            (declare (fixnum typecode))
            (if (>= typecode target::min-vector-subtag)
              (%err-disp $XNDIMS a n)
              (if (< typecode target::min-array-subtag)
                (report-bad-arg a 'array)
                ;;  This typecode is Just Right ...
                (progn
                  (unless (= (the fixnum (%svref a target::arrayH.rank-cell)) n)
                    (%err-disp $XNDIMS a n))
                  (let* ((rmi (%array-index a subs n)))
                    (declare (fixnum rmi))
                    (multiple-value-bind (data offset) (%array-header-data-and-offset a)
                      (declare (fixnum offset))
                      (uvref data (the fixnum (+ offset rmi))))))))))))))

arefはあからさまにプリミティブな内容。

(defun elt (sequence idx)
  "Return the element of SEQUENCE specified by INDEX."
  (seq-dispatch
   sequence
   (let* ((cell (nthcdr idx sequence)))
     (if (consp cell)
       (car (the cons cell))
       (if cell
         (report-bad-arg sequence '(satisfies proper-list-p))
         (%err-disp $XACCESSNTH idx sequence))))
       
   (progn
     (unless (and (typep idx 'fixnum) (>= (the fixnum idx) 0))
       (report-bad-arg idx 'unsigned-byte))
     (locally 
       (if (>= idx (length sequence))
         (%err-disp $XACCESSNTH idx sequence)
         (aref sequence idx))))))

eltは、総称関数ではなく、普通の関数。規格上でもそう決まっている。これもarefを呼んでいる。

文字列ストリームは、level-1/l1-streams.lispに定義がある。read-charしたとき、最終的に呼ばれるのは、おそらくこれ。

(defun string-input-stream-ioblock-read-char (ioblock)
  (let* ((string (string-stream-ioblock-string ioblock))
         (idx (string-input-stream-ioblock-index ioblock))
         (end (string-input-stream-ioblock-end ioblock)))
    (declare (fixnum idx end)
             (simple-string string))
    (if (< idx end)
      (progn (setf (string-input-stream-ioblock-index ioblock)
                   (the fixnum (1+ idx)))
             (schar string idx))
      :eof)))

scharを呼んでいるので、最終的に呼ばれるのはaref。

というわけで、実際の定義も計測結果を裏付ける内容だった。定義を見る限り、ランダムアクセスでも順位は変わらない。文字列ストリームだけ、file-positionで位置を指定する分、他と差が開くかもしれない。

結論としては、Clozure CL 1.6においては、文字列の要素を参照する場合、arefが一番速い。

情報を頂いたので追記。SBCLでは、最適化を掛けると、charやeltがarefに展開されるようで、ディスアセンブルした結果も一致するとのこと。良いなあ。

2010-12-28

Packrat Parser

Common Lispでの実装がPackrat ParsingPackrat Parsing: 遅延版にあった。この方の書く記事は実践的でいつも面白い。

確かに、構造体を利用した遅延版は速いかもしれないんだけど、富豪的なメモリの使い方。パーザの数をn、文字列の長さをlとすると、クロージャをn * l個必ず作らなければならない。遅延評価のHaskellとかなら問題ないのかもしれないけど、Lispだと結構辛い。構造体を作る処理自体も重そう。それと、パーザを定義するごとに構造体のフィールドを増やさないといけないのは、色々と面倒な気がする。マクロで、パーザの定義ごとに構造体を再定義する方法はあるかもしれないけど、ちょっと泥臭い。

自分で使う分には、ハッシュテーブルで十分かとも思うけど、実際にどれだけ高速化するか測ってみないとなんとも言えない。

2010-12-27

ちょっと気の利いた再帰の使い方

考えてみると当たり前なんだけど、軽く感動した再帰の使い方について。

ストリームか文字列を同じ引数として受け取る関数を書こうとして、ストリームの場合と文字列の場合の処理の振り分けについて考えていた。最初は単純に、typecaseや、cl-matchなどのパターンマッチャで、型ごとに分岐させるか、総称関数を使って、型ごとに関数を分けてしまおうと考えた。ただ、処理が、分岐や別の関数に散らばってしまうので、何となくしっくり来なかった。

そこで、他の人がどうやってるのかが気になって、使ってる処理系のコードを読んでみたところ、


(defun f (string-or-stream)
  (if (stringp string-or-stream)
      (with-input-from-string (s string-or-stream)
        (f s))
      (main-process)))

のようなアプローチを採っていた。賢い。

今後、こういうパターンでは使わせてもらおう。

2010-12-20

cond-cons

Scheme:マクロの効用リストの構築で、cond-consというマクロが紹介されてるけど、Common Lispで欲しくなったので書いてみた。

(defmacro cond-cons (&rest clauses)
  (labels ((rec (clauses)
             (if clauses
                 (let ((clause (car clauses))
                       (r (rec (cdr clauses))))
                   `(if ,(car clause) (cons (progn ,@(cdr clause)) ,r) ,r))
                 nil)))
    (rec clauses)))

一瞬、rが複数回評価されて、副作用があるとまずいんじゃ、とか思ったけど、考えたらifで分岐するから問題なかった。

マクロなのに末尾再帰版。すみません。大好きです末尾再帰。

(defmacro cond-cons (&rest clauses)
  (labels ((rec (clauses fn)
             (if clauses
                 (let ((clause (car clauses)))
                   (rec (cdr clauses)
                        (lambda (x)
                          `(if ,(car clause)
                               (cons (progn ,@(cdr clause)) ,(funcall fn x))
                               ,(funcall fn x)))))
                 `(nreverse ,(funcall fn nil)))))
    (rec clauses (lambda (x) x))))

コンパイル時にしか評価されないので、ほとんど意味がない。末尾再帰じゃないバージョンだとスタックが溢れるくらい、clausesが長いリストだったりするなら多少は意味があるだろうけど、それってどんなだよ。

Paul Grahamも同じ趣旨のことをOn Lispかどこかに書いてたように思うけど、マクロ定義のコードで頑張る意味なんてない。

ちなみに、Gaucheでは、util.listにcond-listという名前で、より高機能なものが収録されている。何でそんなことを書くかというと、以前探したときに、しばらく見付けられなかったからではない。そんなわけはない。

2010-12-13

teepeedee2でfaviconを表示

手探りでやるしかないから、えらく苦労した。

(eval-when (:compile-toplevel :load-toplevel :execute)
  (asdf:load-system :teepeedee2))

(defpackage :tpd2-favicon (:use :cl :tpd2 :tpd2.ml.html))
(in-package :tpd2-favicon)

(defconstant +icon-content-type+
  (byte-vector-cat "Content-Type: image/x-icon" tpd2.io:+newline+))

(eval-when (:compile-toplevel :load-toplevel :execute)
  (defun favicon ()
    (with-open-file (in "work/tpd2/favicon.ico" :element-type '(unsigned-byte 8))
      (let ((buf (make-array (file-length in) :element-type '(unsigned-byte 8))))
        (read-sequence buf in)
        buf))))

(dispatcher-register-path
 (find-or-make-dispatcher "localhost:8080")
 "/favicon.ico"
 #'(lambda (dispatcher con done)
     (declare (ignore dispatcher))
     (start-http-response :content-type +icon-content-type+)
     (send-http-response con done (with-sendbuf () #.(favicon)))))

(defsite *favicon-site* :dispatcher (find-or-make-dispatcher "localhost:8080"))

(with-site (*favicon-site*)
  (defpage "/" ()
    (<p "favicon test")))

(http-start-server 8080)
(ccl:process-run-function "tpd2" #'event-loop)

2010-11-03

Clozure CL 1.6 RC1

Clozure CLの1.6 RC1が出てた。

ARMアーキテクチャ対応が一番の目玉だろうけど、さり気にTicket #696Ticket #731が修正されてるのも大きい。これで、1.4以降から続いていたWindowsでの問題もひと段落して、最近のLinuxでsegmentation faultになる問題も解決された。両環境で、trunk以外まともに使えなかった状況が、ようやく改善された形。

あとは、ASDFがASDF2になったり、ネットワーク越しにloadできるようになったり、色々。

2010-10-24

Quicklisp

Zach Beaneが書いたCommon Lispのライブラリ管理システム、Quicklisp。まだベータリリースだけど、使い勝手が素晴らしい。

導入や更新が簡単な上、Windowsでも問題なく動作し、環境への依存性が低い。インターフェイスもシンプルで分かり易い。この明快さは受けると思う。実際、Twitterでは、現時点で好意的な評価が多い。ASDF-Installほどad hocではないし、clbuildと違ってLispで書かれ、バージョン管理システムを求められることもない。LinuxとWindows両方で、すぐにASDF-InstallからQuicklispに切り替えた。

これからの動向にも期待。

2009-12-12

Common Lispでの標準出力へのバイナリデータの出力

SchemeR6RSで揉めているので、Scheme Steering Committeeが何らかの成果を出すまで様子見をしつつ、Common Lispを試してみることにした。結構居心地が良い。SLIMEが便利過ぎる。

ただ、Schemeとの違いに戸惑うことも結構あって、今回は標準出力へのバイナリデータの出力について。

Schemeでは、R5RSのポートはテキストとバイナリの区別が無いので、処理系依存にはなるものの、バイナリデータを素直に書き出すことができる。R6RSでは、テキストポートとバイナリポートが定義され、standard-output-portは標準出力に対応するバイナリポートを返し、current-output-portはテキストポートを返すと決められている。R5RSと互換性を取りつつも、バイナリデータもしっかり扱えるようになっていて、R6RSはこういう所が結構上手くできていると感じる。レコードの仕様とか、とても賛成できないようなものも多いけど。

Common Lispでは、ポートではなくストリームになるわけだけど、テキストとバイナリの区別はちゃんとできる。ただ、標準出力に対応するストリームの*standard-output*が処理系依存なので、標準出力にバイナリデータを出力するポータブルな方法がない。ただ、/dev/stdoutを使ったり、処理系の独自拡張を使えば、処理自体はできたりする。

詰めが甘いというか、この辺、もっとどうにかならなかったんだろうか。パイプでデータを受け渡すやり方って、昔から割と良くあるパターンだと思うんだけども。

ちなみに、今回のきっかけは、処理系が対応してない外部文字エンコーディングのCGIでの取り扱い。変換されたバイト列を、標準出力にバイナリデータとして出力すればいいんじゃないかと思ったら、ひと工夫必要だった、という話。