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

文字列ポートを使おう

プログラミング言語 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

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

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

データ交換用フォーマットとして 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