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-09-05

OCamlのtoplevelを再起動するための設定

#quit;;してM-x tuareg-run-ocamlするのが面倒になってきたので。Emacs Lispは詳しくないから間違った作法とかがあるかも。(特にプロセスの終了を待つあたりとか)

(defun restart-ocaml-toplevel ()
  (interactive)
  (let ((buffer (get-buffer tuareg-interactive-buffer-name)))
    (cond ((null buffer) (tuareg-run-ocaml))
          (t
           (let ((process (get-buffer-process buffer)))
             (when process
               (comint-simple-send process "#quit;;")
               (with-timeout (10)
                 (while (comint-check-proc buffer)
                   (sleep-for 0 500)))))
           (goto-char (point-max))
           (tuareg-run-ocaml)))))

(defun introduce-tuareg-toplevel-restart ()
  (define-key tuareg-mode-map "\C-cr" 'restart-ocaml-toplevel))

(add-hook 'tuareg-mode-hook 'introduce-tuareg-toplevel-restart)