読者です 読者をやめる 読者になる 読者になる

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

僕の考えた最強のデータ交換用フォーマット

データ交換用フォーマットの現状

データ交換用フォーマットとして XML は優秀です。 柔軟な表現が可能であり、一方では厳密な構造を形式的にスキーマで規定することも可能です。 また、考え方は簡潔です。

しかし近頃ではウェブ API では JSON が用いられることが多くなっています。 ウェブを支配している JavaScript との親和性のよさは XML の利点と引き換えにするだけの価値がある場合もあるでしょう。

XML の欠点

XML の難しさのひとつは名前空間にあると私は考えています。 既存の語彙を差し込むことで再利用できる名前空間の有用性はわかりますが (名前空間の機能を持たない) JSON が広く使われている状況を見ると実は無くてもよい場面の方が多いのではないかとも思えます。 また、属性と子要素との区別は HTML (XHTML) などの文章のマークアップには有効に働きますがプログラムがデータ交換するにはあまり意味がありません。

新しいデータ交換用フォーマット SLN の提案

以上を踏まえて、簡単に使えるデータ交換用フォーマットを考えてみました。 人間が読み書きすることを考えず、 XML の面倒な部分を排除したデータ交換用フォーマットです。 XML に似たタグによる構造化を行指向にやることから私はこれに Structured lines notation (略して SLN) と名付けました。 各行が XML で言うところの開始タグ、テキスト要素、終了タグのいずれかであるようなテキストです。

最初に例を示します。 たとえば人物名と年齢の組を XML で表現するとこうなるでしょう。

<members>
 <person>
  <name>James</name>
  <age>16</age>
 </person>
 <person>
  <name>Alice</name>
  <age>10</age>
 </person>
</members>

SLN で表すとこうなります。

+members
+person
+name
.James
-
+age
.16
-
-
+person
+name
.Alice
-
+age
.10
-
-
-

行の最初がプラス記号である場合が XML でいうところの開始タグに相当し、マイナス記号のみの行が終了タグに相当します。 ピリオドで始まる行がテキスト要素です。 プラスの次の要素から改行までがタグ名です。 空白などが含まれる場合はその空白も含めてタグ名として機能します。 同様にピリオドの次の文字から改行までがテキスト要素の内容です。 XML と同じくただひとつのルート要素を持ちます。

SLN では文字コードとしては原則として UTF-8 を採用します。 文字列のエスケープはありません。 特別な解釈をされてしまう文字を普通の文字とした解釈させるためのものがエスケープと考えると、 SLN には行の最初の文字しか特殊な解釈をされる文字がないのでエスケープの必要がないからです。 ただ、以上の仕様だけではタグ名前にもテキスト要素にも改行を含めることができません。 そこでスラッシュで始まる要素は改行を狭んで前要素と繋げるという仕様も追加します。

例として掲示板のデータを考えてみましょう。 XML でいえば以下のようなデータです。

<bbs>
 <message>
  <name>James</name>
  <content>Ladies and gentleman!
How are you?
</content>
 </message>
 <message>
  <name>Alice</name>
  <content>I'm fine.
Thank you.</content>
 </message>
</bbs>

これは SLN では以下のように表現できます。

+bbs
+message
+name
.James
-
+content
.Ladies and gentleman!
/How are you?
-
-
+message
+name
.Alice
-
+content
.I'm fine.
/Thank you.
-
-
-

スラッシュで始まる前の行が開始タグだった場合にはタグ名に改行を含んだものとして処理することも出来ます。

SLN の表現としての改行は 0x0A の 1 バイトのみとしますが、構文解析した結果の改行をどう解釈するかはアプリケーションの裁量とします。 それと、最後の終了タグの行も改行で終わるのは必須とします。

検討を要す事項

終了タグ

マイナスで始まる行は終了タグであると決めましたが、マイナスと改行の間に文字列が有った場合にどのように解釈するべきか決めかねています。 考えられる候補としては以下がありますが、どれも一長一短があるように思います。

  • 認めない。 余計な文字列はエラーである。
  • 開始タグと同じ文字列があるべきである。
  • 開始タグと同じ文字列があるべきであるが、省略することも許す。
  • 単に無視する。

プログラムが読み書きすることだけを想定しているので、エラー検出を目的とした冗長な文字列はあまり意味がないでしょう。 余計な文字列はエラーとするのが望ましいのではないかというのが私なりの考えです。

記号

各行の最初の文字列をプラス、マイナス、ピリオド、スラッシュとしました。 これらは感覚的に妥当でしょうか?

空行

ルート要素の開始タグの前の空行、終了タグの後の空行を許すべきでしょうか?

運用

ルート要素

ルート要素の名前は実質的にフォーマットを表わす名前として機能します。 汎用的なフォーマットはバージョンナンバーまで含めた詳細な名前をルート要素のタグ名にするのが望ましいと考えます。

順序

SLN では連続するテキストを別の行にするだけで複数のテキスト要素とすることが出来ます。 出現順序に意味付けすることでタグを付けずに済ますという方法は考えられます。

つまり、最初の例をこのように表現するというのもひとつの案です。

+members
+person
.James
.16
-
+person
.Alice
.10
-
-

person 要素の子要素として最初に現れるテキスト要素を名前、次に現れるテキスト要素を年齢ということにすればそれぞれをタグで囲わずに済ませることが出来ます。 データ量が多少節約できるかもしれません。

拡張の余地

行の最初の文字でその行が意味するところを表わすという設計なので、適当な文字に意味付けすれば様々なものを入れられるでしょう。 たとえば $ で始まる行に入っているのは数値型とするというようなことも可能です。 私としては (XML がそうであるように) 要素の意味付けはアプリケーションでやるべきだという考えですが、拡張できる余地は有るに越したことはないでしょう。

Document ID: 4326062d3d9bca78c0a03fc9c1d0f19c

字面と意味と

私はラテン文字の単語を文章中に書くのが好きではない。 不自然でない程度に伝統的な日本語の単語に置き換えれないか考えるし、それが出来ないならカタカナ語で書く。 とはいっても固有名詞はそのまま書くこともあるし、プログラミングの話題も多いので具体的な字面に意味がある言葉はそのまま書くしかない。 たとえば Scheme の syntax-rules という構文のことをシンタックス・ルールなんて書かないし、もちろん構文規則という言葉に置き換えることもない。

さて、私はこの考え方に基いて Scheme で手続き (関数) を生成する式という概念をいうときは「ラムダ式」と書き、具体的に lambda という構文を用いて書かれている式のことは「lambda 式」と書いたりする。 Scheme という言語の中ではそれは実質的に同じことを指しているのだから区別する意味などないのかもしれないが、私なりの美学だ。

私は日本人 (更に言えば英語を不得意としている) であるから、日本語に置き換えるときに字面と意味とを区別して考えているが、英語をよく理解して英語の文章で読み書きしている場合にはこの区別は曖昧になりそうだということをふと思った。 (Scheme の仕様書では字面に意味がある言葉は等幅書体で書かれているので区別はされている。)

Document ID: 2f56765df19a4de2b2a1e3088b727e69

Scheme の begin と継ぎ合わせ

プログラミング言語 Scheme の構文である begin は直列化のための機能としてよく理解されている。 要するに複数の式を順番に評価させる (そして最後の式の評価結果を全体の評価結果として返す) ための構文である。 Common Lispprogn に相当するものと説明されることもある。

だが、 begin は状況によって別の解釈をされることがある。 R5RS の「5.1.プログラム」の項目で定義されているのだが、トップレベルに現れる begin フォームに限っては、その内側に書かれている式、定義、構文定義と等価であるとされている。 つまり、 (begin a b c) と書かれていたら、それはトップレベルに a b c と書かれたのと同じになるということだ。

具体例で表してみよう。

(begin
  (define (plus-one x)
    (+ x 1))

  (define (plus-two x)
    (+ x 2)))

(write (+ (plus-one 1) (plus-two 1)))

このようにトップレベルの begin の内側に書かれたものは以下と同等に解釈される。

(define (plus-one x)
  (+ x 1))

(define (plus-two x)
  (+ x 2))

(write (+ (plus-one 1) (plus-two 1)))

要するにトップレベルで begin に囲まれている個所は囲まれていないのと同じであるということだ。 この操作を begin の外側に「継ぎ合わせ (splicing)」られるという。 このことを知らないと begin の内側での定義は一見してローカルな定義に見えてしまいがちなので注意が必要である。

囲んでも囲まなくても同じならわざわざ begin で囲む意味などないだろうと思うかもしれないが、これは主にマクロへの配慮だと考えられる。 マクロはひとつのフォームを別のひとつのフォームに変換するものだが、複数のフォームへ変換したい場合もあるので見掛け上のひとつのフォームにするために begin が必要なのだ。

ちなみに、 R6RS や R7RS ではこの継ぎ合わせはトップレベルだけでなく <body> 部で機能するように拡大されているのと、意味の異なる二種類の構文であることがより明確に書かれている。 また、 R7RS には define-library の補助構文としての begin もある (つまりは三種類の begin がある)。 それらは同じ begin という名前でありながらも現れる場所によって違う解釈されることには充分に注意が必要である。

Document ID: 0a54a00152f1c3e16c15ea8e11b1ea7b

ファイル名の順序

パソコンを使っているとファイル名一覧を見る機会は頻繁にある。 ファイル名一覧を出力する多くのソフトは見易いように項目の内容でソートする機能を持っている。 ファイル名の順序で並び換える場合は一般的には「辞書順」である。 辞書順というのはふたつの文字列の先頭から比較していって異なるところの文字を比較することで文字列の大小が決定される方法である。

ウィンドウズのエクスプローラもソート機能を持っているが、ファイル名の順序でソートしたときにファイル名に含まれる数値の部分を特別扱いする。 たとえば abc5.txt というファイルと abc40.txt というふたつのファイルが有ったとき、伝統的な辞書順では abc40.txt の方が小さいが、エクスプローラabc5.txt の方を小さいと判断する。 一般的な人間の感覚としてはその方が感覚に一致するのだろう。 私は古い世代の人間であるから、いまだにゼロパディングして桁数を合わせてしまうのだが。

なんとなく思い立ってこのルールを Scheme で実装してみた。 R7RS の形式にしてある。

(define-library (filename-compare)
  (export filename<?
          filename>?
          filename=?
          filename<=?
          filename>=?)
  (import (scheme base)
          (scheme char))
  (begin
    (define (read-integer port)
      (do ((ch (peek-char port) (peek-char port))
           (acc 0 (+ (* acc 10) (digit-value ch))))
          ((or (eof-object? ch) (not (char-numeric? ch))) acc)
        (read-char port)))

    (define (filename-compare str1 str2)
      (let ((port1 (open-input-string str1))
            (port2 (open-input-string str2)))
        (let loop ((ch1 (peek-char port1))
                   (ch2 (peek-char port2)))
          (cond ((eof-object? ch1)
                 (if (eof-object? ch2) 'eq 'lt))
                ((eof-object? ch2)
                 'gt)
                ((and (char-numeric? ch1) (char-numeric? ch2))
                 (let ((num1 (read-integer port1))
                       (num2 (read-integer port2)))
                   (if (= num1 num2)
                       (loop (peek-char port1) (peek-char port2))
                       (if (< num1 num2) 'lt 'gt))))
                ((char-ci=? ch1 ch2)
                 (read-char port1) (read-char port2)
                 (loop (peek-char port1) (peek-char port2)))
                (else
                 (if (char-ci<? ch1 ch2) 'lt 'gt))))))

    (define (filename<? x y) (eqv? (filename-compare x y) 'lt))

    (define (filename>? x y) (eqv? (filename-compare x y) 'gt))

    (define (filename=? x y) (eqv? (filename-compare x y) 'eq))

    (define (filename<=? x y)
      (let ((r (filename-compare x y)))
        (or (eqv? r 'lt) (eqv? r 'eq))))

    (define (filename>=? x y)
      (let ((r (filename-compare x y)))
        (or (eqv? r 'gt) (eqv? r 'eq))))
    ))

これは私の理解を形にしたものであって、ウィンドウズの実装と一致することを検証しているわけではない。

Document ID: 7d30c7ea6aad074df0def7719562b113