Gauche-epub

電子書籍ファイルフォーマットのひとつである ePub の生成を Scheme 処理系 Gauche から行う拡張パッケージ Gauche-epub を公開した。

ePub は html 形式の文章やスタイルシートや画像にいくつかのメタ情報を併せて ZIP アーカイブ形式でまとめた形になっていて、 Gauche-epub は ZIP アーカイブを操作するために Gauche-zip-archive パッケージを利用するので事前にこちらも導入しておく必要がある。 現時点での開発版 (git リポジトリの HEAD) の Gauche を想定しているので、古い Gauche だと動かない部分があるかもしれない。

今の Gauche-epub はまだあまり使い込んでおらず、これからかなり変更を加える可能性がある。 故にドキュメントも用意できていない。 使い方を把握するには同梱している用例を見るのが手っ取り早いと思う。 以前に「文章構造」という表題で書いた構想を元にしている。

Gauche-epub はメタ情報の生成を自動化するものであって、与えられたコンテンツ、すなわち html が ePub として正しいかといったようなことはアプリケーション側で管理する必要があることには留意すべきだ。 アプリケーションが生成した ePubepubcheck といったようなバリデータで確認するのは良い習慣だと思う。

Document ID: 042b5647c17d408eb2a6524d353e7c3b

短縮記法の活用

Common Lisp でのリーダーマクロの興味深い利用例の紹介を見た。

#:g1: リーダーマクロでシンボルの略記をする

プログラミング言語 SchemeLISP 系言語といえども Common Lisp でいうところのリーダーマクロに相当するものはない。 しかし、いくつかの短縮記法は提供されていて、読み込みの段階で対応する名前に変換される。 R5RS や R7RS では以下のよっつがそうだ。

短縮記法 対応する名前
' quote
` quasiquote
, unquote
,@ unquote-splicing

それに加えて R6RS では以下の短縮記法も用意されている。

短縮記法 対応する名前
#' syntax
#` quasisyntax
#, unsyntax
#,@ unsyntax-splicing

これらは単なる短縮記法であるので、たとえば quote の意味を置換えてしまえば ' の意味も変えられる。 新しい記法を追加することは出来なくても、既存の記法の意味を変えることは出来るのだ。

私は以前に quasisyntax を置換える試みをしたことがある。

文字列補間 - 主題のない日記

使いどころを慎重に考えないと混乱の元になりそうだが、コードゴルフくらいには使えるかもしれない。

Document ID: 2882acdda16167cb2246cea8dd2b1ed0

文字列ポートを使おう

プログラミング言語 Scheme において少し気になる string-append の使い方の事例を見た。

これは、私が以前に append が気になるとして記事にした事例とよく似ている。 図を描くのが面倒なのであらためて解説はしないが、空間的にも速度的にも本来なら O(n) のオーダーで出来るはずの処理を O(n2) にしているという意味で同じである。 要するに無駄な中間状態を作っているのだ。 (注意:処理系が文字列の実装に rope などを使っている場合はそんなに差は生じない。 ここでは素朴な処理系を前提としているが、一見して非効率なコードに見えても処理系によっては効率的な場合もある。)

R5RS の範囲内で平均的に性能が良い方法はおそらく結合すべき文字列をリストにまとめてから一気に string-append で結合する方法だと思う。 (末尾再帰にするともっといいかもしれない。)

(define (string-join-with-infix-and-newline string-list delim)
  (apply string-append
         (if (null? string-list)
             '()
             (letrec ((a (lambda(x) (if (null? x) '("\n") (cons delim (b x)))))
                      (b (lambda(x) (cons (car x) (a (cdr x))))))
               (cons (car string-list) (a (cdr string-list)))))))

さて、ここからが本題の文字列ポートの紹介である。 もし処理系が文字列ポートをサポートしているならそれが使える。 文字列ポートは SRFI-6 (Basic String Ports) として提案され、 Script-Fu を含む多くの処理系で採用されている。 R6RS や R7RS では仕様自体に取り込まれた。 (R6RS の文字列ポートは SRFI-6 とは使い方が異なることに気をつけること。)

上述のコードを文字列ポートを使って書き換えるとこのようになる。

(define (string-join-with-infix-and-newline string-list delim)
  (if (null? string-list)
      ""
      (let ((port (open-output-string)))
        (display (car string-list) port)
        (for-each (lambda(x) (display delim port) (display x port))
                  (cdr string-list))
        (newline port)
        (get-output-string port))))

あたかもファイルに出力するのと同じような書き方で文字列ポートに蓄積して、最後に文字列として取り出すという操作で文字列を構築する。 I/O と共通の操作で文字列構築ができるというのはとても使い勝手がよいし、一般的には性能もよい。 Scheme で文字列の処理をするには文字列ポートはなくてはならないものだと私は感じている。

Document ID: 11a193e8d0f408bb591f09f17be3c315

フックポイントとしての総称関数

プログラミング言語 Scheme の仕様にはオブジェクト指向的な支援が含まれない。 しかし、独自の拡張として持っている場合もある。 あるいは後付けでオブジェクト指向的な機能を追加するライブラリもある。 オブジェクト指向というのも様々な種類があるが、 Scheme で使われることが多いのは Common Lisp に含まれるオブジェクトシステム (CLOS) を真似たものであることが多い。 もちろん前提となる言語が違うのであるから、 Scheme の、あるいは処理系の都合に合わせて改変が加えられてはいる。

Scheme 処理系のひとつである Gauche もまた CLOS 風のオブジェクトシステムを持っていて様々に活用されている。 Gauche を使っていて特に頻繁に使われる総称関数 (Scheme の用語では「関数」よりも「手続き」が使われるが、 CLOS の習慣に倣ってかドキュメントには「総称関数」という用語が用いられている) は ref だろうか。 ref は複数の要素をまとめるような型のオブジェクトに対して同じような使い勝手で適用でき、要素を抜き出すものだ。

(ref '(a b c d) 2)
(ref '#(e f g h) 3)
(ref "ijkl" 1)

このように ref はリスト、ベクタ、文字列に使える。 その他にもハッシュテーブルやツリーマップなどにも対応している。 必要であればメソッドを追加して新しい型に対応させることも出来る。

ref の場合は同じような意味を持つ手続きをひとつの名前にまとめたというだけのものだが、アプリケーションに機能を追加するためのフックポイントとしても活用できる。 たとえば先日に紹介した Gauche-zip-archive という拡張において現状では zip-add-entry だけは総称関数として提供していて、これはアーカイブにエントリを追加するにあたって前後に処理を追加したい場合を想定しているからだ。 例として、エントリの名前に必ず拡張子 .txt を追加するというような処理を入れたい場合にはこう書ける。

(use zip-archive)

(define-class <extended-zip> (<output-zip-archive>)
  ())

(define-method zip-add-entry
    ((archive <extended-zip>) (name <string>) (content <string>))
  (next-method archive (string-append name ".txt") content))

(let ((archive (make <extended-zip> :name "test.zip")))
  (zip-add-entry archive "entry1" "hoge-huga-hige")
  (zip-close archive))

このようなカスタマイズ方法は、 Gauche で書かれている Wiki システムである WiLiKi でも活用されている。 私はそれを見て Gauche-zip-archive にも導入してみた次第である。 現時点では ZIP フォーマットのごく基本的な部分にしか対応していないライブラリではあるが、メソッドの追加という形で ZIP フォーマットの様々な拡張に対応できる可能性を提供したつもりだ。 (自分ではやりたくないのでという本音。)

Document ID: 21bf69adf19ce4d9fa17f6c7ad405d68

Gauche で ZIP を扱う

アーカイバとして ZIP は特に広く普及した形式のひとつだろう。 スクリプトから操作したいことはしばしばあり、私は Gauche で ZIP を生成する拡張パッケージを作っていた。

https://github.com/SaitoAtsushi/Gauche-zip-archive

ZIP を読む方の機能もつい最近になって追加した。 ZIP を読むことについて以前に記事にしたことがあるし、それ自体はそんなに難しいことというわけではないのだが、現実に使われている ZIP には様々な拡張があったり、ときには壊れていたりもするのでどこまで対応すべきか考えるのが面倒で放置していた。 最終的にはごく基本的なものだけ読めればよいと割り切ることにした。 少なくともこのライブラリで作ったアーカイブはこのライブラリで読めるようにということは想定している。 それ以上に高機能で堅牢なものが必要であれば適当なライブラリのバインディングを用意した方が簡単だろう。

さて、ライブラリにはドキュメントもつけているが、英語で細かいことを書く能力がないのでごく簡単なものでしかない。 ここでもう少し解説しておくことにする。 (インストール手順は省略する。)

スクリプトからライブラリの機能を使うためには当然だがライブラリを use しなければならない。 ライブラリの名前は zip-archive なので (use zip-archive) と書くことで準備できる。

ライブラリが含む手続き群はふたつに分けられる。 アーカイブを作るためのものと読むためのものである。 まず作る方から取り上げる。 アーカイブを作る基本的な手順は open-output-zip-archiveアーカイブをオープンし、 zip-add-entry でエントリを追加して zip-close で閉じるというものになる。 例としては以下のような使い方が基本となる。

#!/usr/bin/env gosh
(use zip-archive)
(use rfc.zlib)

(let ((za (open-output-zip-archive "test.zip")))
  (zip-add-entry za "1.txt" "number one.")
  (zip-add-entry za "2.txt" "number two.")
  (zip-add-entry za "3.txt" "number three." :compression-level Z_NO_COMPRESSION)
  (zip-close za))

圧縮レベルを指定することもでき、これは open-deflating-port で使える指定と同じである。 1 から 9 のいずれかの整数を指定するか、 rfc.zlib ライブラリで定義されている定数を用いる。

アーカイブのクローズを忘れると正常なアーカイブが作られないので、可能なら call-with-output-zip-archive を使うのが望ましい。

#!/usr/bin/env gosh
(use zip-archive)

(call-with-output-zip-archive "test2.zip"
  (lambda(za)
    (zip-add-entry za "one.txt" "number 1.")
    (zip-add-entry za "two.txt" "number 2.")
    (zip-add-entry za "three.txt" "number 3.")))

といった具合だ。

次に ZIP の読み込みについて取り上げる。 open-input-zip-archive で開いたアーカイブから zip-entries で取出したエントリ群 (エントリをリストにしたもの) から具体的な値を取出すのが基本的な手順になる。 たとえばアーカイブに含まれるエントリのファイル名を表示するのはこう書ける。

#!/usr/bin/env gosh
(use zip-archive)

(let ((za (open-input-zip-archive "test.zip")))
  (for-each (lambda(entry)
              (display (zip-entry-filename entry))
              (newline))
            (zip-entries za))
  (zip-close za))

この例ではエントリの名前を表示したが、 zip-entry-timestamp でタイムスタンプ、 zip-entry-datasize で展開後のデータの大きさ、 zip-entry-body でエントリに格納されている内容を表示できる。 (タイムスタンプとして格納されている値は標準時として解釈するようにしているが、現実にはローカルタイムの場合もあるようだ。)

これもアーカイブを作成するときと同様にクローズを忘れないよう call-with-input-zip-archive を使うのが望ましい。 また、 open-input-zip-archive が生成するオブジェクトはコレクションクラスを継承しているので、 gauche.collection モジュールで定義されている総称手続きにそのまま渡すことができる。 これらのことを利用して書き替えるとこうなる。

#!/usr/bin/env gosh
(use zip-archive)
(use gauche.collection)

(call-with-input-zip-archive "test.zip"
  (lambda(za)
    (for-each (lambda(entry)
                (display (zip-entry-filename entry))
                (newline))
              za)))

以上が Gauche-zip-archive パッケージの機能の全てである。

Document ID: 471a3123363ee8deb54e703682ad7958

オプショナルな多値

プログラミング言語 Scheme では、手続きが複数の値を返すことが出来て、それは多値と呼ばれている。 多値を受取るプリミティブな構文は call-with-values である。 R6RS では let-valueslet*-values もあるし、 R7RS ではそれらに加えて define-values もある。 更に SRFI-8 には receive もある。

しかしこれらの構文は返ってくる値の数が決まっていない場合には使い難い。 ある手続きが返す値が一個だったり二個だったりするような場合が綺麗に表現できない。 そこで、先日紹介した let-optionals* のように、値の数が少ない場合にデフォルト値で埋めるような構文があると便利かもしれないと考えて書いてみた。 下請けに let-optionals* を使うのでそれも含めて R7RS 形式のひとつのライブラリにまとめた。

(define-library (optional)
  (export let-optionals*
          receive-optionals*
          :optional)
  (import (scheme base))
  (begin

    (define-syntax let-optionals*
      (syntax-rules ()
        ((_ args ((var default) . rest) body0 body* ...)
         (let* ((temp args)
                (var (if (null? temp) default (car temp))))
           (let-optionals* (if (null? temp) '() (cdr temp)) rest
             body0 body* ...)))
        ((_ args () body0 body* ...)
         (if (null? args)
             (begin body0 body* ...)
             (error "Too many arguments:" args)))
        ((_ args (var . rest) body0 body* ...)
         (let-optionals* args ((var #f) . rest) body0 body* ...))
        ((_ args rest-var body0 body* ...)
         (let ((rest-var args)) body0 body* ...))))

    (define-syntax :optional (syntax-rules ()))

    (define-syntax %receive-optionals*
      (syntax-rules (:optional)
        ((_ (mandatory-args ...) (:optional . args) expression body0 body* ...)
         (call-with-values
             (lambda () expression)
           (lambda(mandatory-args ... . rest)
             (let-optionals* rest args
               body0 body* ...))))
        ((_ (mandatory-args ...) (head-arg . args) expression body0 body* ...)
         (%receive-optionals*
          (mandatory-args ... head-arg) args expression body0 body* ...))
        ((_ (mandatory-args ...) last-arg expression body0 body* ...)
         (%receive-optionals*
          (mandatory-args ... . last-arg) () expression body0 body* ...))))

    (define-syntax receive-optionals*
      (syntax-rules ()
        ((_ formals expression body0 body* ...)
         (%receive-optionals* () formals expression body0 body* ...))))
    ))

以下のように使える。

(import (optional)
        (scheme base)
        (scheme write))

(display
  (receive-optionals* (a b :optional c (d 1))
      (values 1 2 3)
    (list a b c d)))

こういう場合は手続きの方で戻り値の個数を合わせた方が良い設計に思えるので、あまり出番はないかもしれない。

Document ID: 1337c570221640fead1c0f2c911d5da5

Scheme における戻り値の未規定

プログラミング言語 Scheme では式を評価することで値が得られる。 そしていくつかの手続きは戻り値が未規定である。 JavaScript などでは「未定義値という値」が存在することと、 Scheme のいくつかの実装でも同様にそのようなオブジェクトが使われていることからしばしば誤解されることがあるようなのだが、 Scheme でいうところの「戻り値が未規定」というのは何も決まっていないという意味である。

未規定なのは処理系の裁量で好きにしていいのだ。 各手続きの処理の中で使えそうな情報を返すような実装でも許されるし、デタラメな数値かもしれないし、とりあえず真偽値でも返しておくというのもかまわない。 実際には REPL で対話的にプログラミングしているときに無関係な出鱈目な値が出てくるのはうんざりするということもあってか、未規定であることを表すオブジェクトを返す処理系は多い。 その他、 '()#f を返すような実装も一般的である。

ちなみに、 R5RS や R7RS では「式の結果は未規定 (the result of the expression is unspecified)」という表現になっているのだが、 R6RS では「未規定値を返す (return unspecified values)」という表現になっている。 私は英語を不得意としているから微妙な意味合いを捉えることが出来ないのだけれども、もし JavaScript などに慣れた人が見たら R6RS は未規定を表すオブジェクトがあるように誤解しやすい表現であるように思う。 一方で、 R5RS や R7RS で用いられている result という語は副作用も含めた結果を連想させてしまうと思う。 言葉の意味するところについて説明している項目はあるのだが、(Scheme の仕様はそう多くもない分量とはいえども) 隅々まで目を通している方が少数派であろうし、知っていても言葉の印象に引き摺られるということはあるかもしれない。

更に、 R6RS で表現が異なっているのは文章表現が異なっているだけではない。 よく見ると values という複数形で書かれているのである。 R6RS で、ある手続きが未規定値を返すといった場合にはそれが多値である可能性がある。 このことは 5.9 Unspecified behavior に明記されている。 実際に多値な実装を見たことはないが、戻り値が未規定の件については R5RS や R7RS より R6RS の方がより未規定というわけだ。

他の言語でもそうだが、仕様で未規定な部分は不意に処理系が挙動を変えてしまったり、処理系ごとに挙動が違ったりする。 意識しておかないと処理系が更新したときに急に動かなくなってしまうこともあるし、移植性に劣るものになってしまうかもしれない。 こういった未規定な値に依存しないように注意が必要だ。

Document ID: a58c1648bb0f2db1ea76d5e7b78a1fa8