Gaucheクックブック

Gauche (ゴーシュ)クックブックは動作する短いコードを一問一答形式で提示していくスタイルのプログラム解説ページです。毎週月曜、木曜に更新。

2007-09-24

HTTPヘッダフィールド名をきれいに整形する

問題

HTTPヘッダーフィールド名はたいてい単語の先頭1文字が大文字になっているが、なかにはWWW-Authenticateのような例外も存在する。RFCに載っている名前に整形したい。

答え

例外だけは特別に扱い、それ以外のフィールド名はstring-titlecaseで整形します。

(use srfi-13)  ; string-titlecase string-downcase

(define (header-titlecase name)
  (cond ((assoc (string-downcase name)
                '(("etag" . "ETag")
                  ("content-md5" . "Content-MD5")
                  ("te" . "TE")
                  ("www-authenticate" . "WWW-Authenticate")))
         => cdr)
        (else (string-titlecase name))))
  
(header-titlecase "etag")          ; => "ETag"
(header-titlecase "user-agent")    ; => "User-Agent"

condのテスト部でassocを使って連想リストを探索し、cdrにそのまま"=>"記号を使って渡すというのは、頻出するイディオムです。assocは探索に成功するとペアを返すので、値だけを取り出すにはそこからさらにcdrを適用しなければならないのですが、これがうまくcondの"=>"を使ったフォームと連係できるのです。

2007-09-20

Weakベクタを使う

問題

JavaやRubyには、weak referenceといって、指している先のオブジェクトがGCされることを許す参照がある。Gaucheにもそういう機能はあるか。

答え

Gaucheでweak referenceは、weakベクタとして実装されています。Weakベクタからしか参照されていないオブジェクトは回収される可能性があり、参照している先のオブジェクトがGCされた場合、そのエントリには#fがセットされます。

(define wv (make-weak-vector 2))   ; => #,(<weak-vector> 1 #f #f)
(weak-vector-set! wv 0 (cons 1 2)) ; => #,(<weak-vector> 2 (1 . 2) #f)
(gc)                               ; GCを起動
wv                                 ; => #,(<weak-vector> 1 #f #f)

GCの振る舞いには不確かなところがあるので、Weakベクタからしか参照されていないからといって、オブジェクトが必ず回収されるということは期待しないでください。

解説

Weakベクタは、Gauche内部でポートのセットを保持するために使われています。flushは全てのポートをフラッシュすることになっているので、ポートのセットをどこかに持たせておかなければなりませんが、それを普通の(強いリファレンスの)ベクタやリストで保持すると、使用されなくなったポートがGCされないという問題が発生してしまいます。その問題を解決するためにWeakベクタが使われています。

Gaucheの上でプログラミングを行うユーザも、まさに上のポートの話と同じような場面に出会うことがあります。たとえば、インスタンスのセットを自動的に保持するようなクラスを考えてみます。そのようなクラスは次のように、インスタンスの初期化の際にクラススロットにインスタンスを登録していくクラスとして実装できます(Wiliki: Scheme:MOP:InstancePool)。

(define-class <foo> ()
  ...
  (all-instances :allocation :class :init-value ())
  )

(define-method initialize ((self <foo>) initargs)
  (next-method)
  (push! (slot-ref self 'all-instances) self)))

しかし上のクラスでは、インスタンスがGCされなくなってしまいます。そこでweakベクタを使って、インスタンスが回収されることを許すクラスを設計できます。

(define-class <foo> ()
  ((all-instances :allocation :class
                  :init-form (make-weak-vector 8))))

(define-method initialize ((self <foo>) initargs)
  (next-method)
  (let* ((wv (slot-ref self 'all-instances))
         (wvlen (weak-vector-length wv)))
    (let loop ((i 0))
      (cond ((= i wvlen)
             ;; Weakベクタがフルなので拡張する
             (let1 new (make-weak-vector (* 2 wvlen))
               (dotimes (i wvlen)
                 (weak-vector-set! new i (weak-vector-ref wv i)))
               (set! (slot-ref self 'all-instances) new)
               (weak-vector-set! new (+ wvlen 1) self)))
            ;; 空のエントリにインスタンスを登録する
            ((not (weak-vector-ref wv i))
             (weak-vector-set! wv i self))
            (else
             (loop (+ i 1)))))))

(make <foo>)を評価してインスタンスを作成し、(class-slot-ref <foo> 'all-instances)でインスタンスのセットに登録されているのを確認してください。その後(gc)を評価すると、インスタンスのいくつかがセットから削除されているのが確認できるはずです。

なお、WeakベクタのAPIでは、Weakリファレンスが#fで置き換えられたのか、最初から#fがセットされていたのか、区別はできません。区別ができなくてかまわないような用途に利用してください。

参照

2007-09-17

子プロセスの出力を文字列として取得する

問題

Perlの`command`のように、子プロセスを実行してその出力を文字列として得たい。

答え

process-output->stringを使いましょう。process-output->stringには、子プロセスがエラー(0以外の終了ステータス)を返したときにエラーを投げたり、エンコーディングの変換を行う機能もあります。

下は、引数で与えられたURLをwgetで取得して、その先にあるコンテンツを文字列で返す手続きです。(ちなみにhttp-getはHTTPS URLに対応していないので、wgetを呼ぶというのは現実的な例です。)

(use gauche.process)
(define (wget url)
  (process-output->string `(wget -O - -q -- ,url)))

解説

process-output->stringの第1引数はコマンドで、上の例ではリストです。リストの各要素に対してx->string(文字列化を行う手続き)が呼ばれるので、シンボルを直接書いておくことができます。(ただし面白いことに、"-i"は実数部と虚数部の省略された複素数として読み込まれるので、これはダブルクオートで文字列にするか、|-i|としてクオートされたシンボルとして書く必要があります。)

リストの替わりに文字列を与えるとそれがシェルに渡されるので、シェルの機能(ワイルドカードの展開やリダイレクトなど)を利用できますが、一方でコマンドラインがどう展開されるのか理解しにくくなるので危険性は高くなります。たとえば、上の例のリストを#`"wget -O - -q -- ,url"という文字列に置き換えたとして、ユーザから"http://yahoo.co.jp; rm -rf /"という文字列が与えられたら、ファイルが全部削除されてしまいます。余分な展開ステップのことを考えたくなければ第1引数をリストにしておくのがお勧めです。

文字列のエンコード変換には:encodingキーワード引数を与えます。:encoding "*JP"としておくと日本語のエンコーディングの自動判定が行われます。"EUC-JP"、"Shift_JIS"、"UTF-8"といった引数のほか、iconvが認識するエンコーディング名も使えます。詳しくは文字コード変換を参照してください。

終了ステータスが0以外だと、デフォルトでは<process-abnormal-exit>コンディションが投げられます。エラーをハンドルするには、guardフォームを使ってコンディションをキャッチしてもよいですし、:on-abnormal-exit :ignoreを指定してコンディションを投げないようにしてもよいでしょう。

参照