Chez Scheme でコンソールに日本語を出力する

プログラミング言語 Scheme の処理系のひとつである Chez Scheme仕様 (R6RS) が定める三種類の符号での入出力をサポートしています。

更に、独自拡張として iconv による変換も可能ですので、ほとんどどんな文字コードでも使えると思ってもいいでしょう。 もちろん、日本語の文字コードとしてよく使われる Shift_JIS やら CP932 やらにも対応しています。

さて、 R6RS では標準入出力用のポートにどんな文字コード変換器が結びついているか、あるいは結びついていないかは未定義ですが、 Chez Scheme では UTF-8 を採用しています。 しかし、 Windows の標準出力に UTF-8 のテキストを流し込んでもコンソール上では化けてしまいます。 それを解決するために CP932 に変換してからの出力を試みている事例を見掛けました。

ですが、これでは CP932 の文字セットに含まれない文字の情報が消えてしまいます。

WindowsUnicode 対応に積極的で、文字列のやりとりが必要な API はほぼ全て Unicode 版が存在しますし、コンソール関連の API についてもまたそうであるのですが、互換性の制約からか、標準出力経由でコンソールに表示する文字は CP932 と解釈してしまうようになっているのであって、コンソール関連の API を直接使えば Unicode のままで利用できます。

そこで、 Chez SchemeForeign Interface を用いて Unicode の文字をコンソールに表示するライブラリを作ってみました。 カスタムポートとして定義しており、このポートに対して出力することでコンソールに表示されます。 標準出力をリダイレクトしていてもコンソールに出力します。

(library (console-port)
  (export open-console-output-port)
  (import (chezscheme))

  (define dummy (begin (load-shared-object "kernel32.dll") 1))

  (define-ftype handle void*)

  (define open-existing 3)

  (define create-file
    (foreign-procedure __stdcall "CreateFileW"
      (wstring unsigned-32 unsigned-32 void* unsigned-32 unsigned-32 void*)
      void*))

  (define file-share-write 2)

  (define generic-write #x40000000)
  
  (define (get-active-console-buffer)
    (create-file "CONOUT$" generic-write file-share-write 0 open-existing 0 0))

  (define write-console
    (foreign-procedure __stdcall "WriteConsoleW"
      (void* wstring unsigned-32 u32* void*)
      boolean))

  (define (open-console-output-port)
    (let ((output-handle (get-active-console-buffer))
          (vsize (make-bytevector 4)))
      (define (write-to-console string start count)
        (let ((str (substring string start (+ start count))))
          (write-console output-handle str count vsize 0)
          count))
      (make-custom-textual-output-port "console" write-to-console #f #f #f)))
  )

使い方としては、 open-console-output-port 手続きが返すポートに書き込むだけです。

(import (rnrs)
        (console-port))

(let ((port (open-console-output-port)))
  (display "あいうえお\nかきくけこ\n" port)
  (display "♘♞♙♕♟♝♜♗♛♚♖♔" port)
  (flush-output-port port)) ;; フラッシュを忘れずに!

CP932 の範囲外の文字もきちんと表示できています。 ただし、コンソールに適用しているフォントがグリフを持っていない場合もあるので Unicode にある文字を全て確実に表示できるというわけではありません。

Document ID: fb260a71b629c76209cdc797c43c9bdd

ゲーム「ラビリンスの彼方」の戦闘システムが良かった

ニンテンドー 3DS のゲーム「ラビリンスの彼方」を購入した。 店頭で見てなんとなく表題が良いと感じて買ったのであって、どのようなゲームなのかは箱に書いてある以上の情報を調べなかったのだが、実際やってみると良くできているのだ。 襲ってくる敵を倒しながら迷宮の塔をのぼっていくゲームで、戦闘システムはジャンケンを元にした三竦みの属性を上手く活用している。

ウェブ上で評判を調べてみると戦闘が面倒くさいという意見もある。 しかし、むしろこのゲームは戦闘以外に見るべきものはあまりない。 迷宮にはそれほど凝った仕組みもなくただ延々と進んでいくだけである。 物語は迷宮を進ませる理由付け程度にしかない。 強いて言えば「跳ね橋」「鍵」「隠し扉」といっや要素はある。 跳ね橋は一方向からしか降ろすことが出来ないが、一旦降ろすと降りっぱなしであり、拠点との行き来を少し短かい距離で出来たりする。 錠で守られた扉は別のどこかで鍵を入手してからでないと開けられない。 隠し扉は一見して壁に見える場所を調べると通路だったりする。 隠し扉はそれほど隠れていなくて、割とわかりやすい。

戦闘システムについてであるが、この作品世界では「グーテシウム」「チョキロム」「パーティリウム」の属性を敵味方全員が持っているのが特徴である。 明らかにジャンケンをモチーフにしている。 ここではわかりやすいように「グー」「チョキ」「パー」と呼ぶことにしよう。 グーがチョキに勝ち、チョキがパーに勝ち、パーがグーに勝つのはジャンケンと同じだ。 つまり、たとえばチョキの属性を持つ者にグーの属性を持つ者が攻撃すれば威力が倍増する。 同じ属性であれば普通程度にダメージを与えることができ、相手側の方が強い属性であれば半分のダメージしか与えることができない。

では、なるべく相手の弱点で攻撃すればいいのだろうかといえばそうではない。 弱点を攻撃した (あるいはされた) ときにはダメージ分のエネルギーがその場に放出される。 たとえばチョキの属性を持つ者がグーの属性を持つ者に攻撃するとグーの属性のエネルギーがその場に放出される。 放出されたエネルギーはその属性の者の攻撃ターンに吸収されてダメージが回復する。 つまり、せっかくダメージを与えてもその後の攻撃順序によっては取り戻されてしまうことが有り得るのだ。 逆にダメージを受けても取り返せる可能性がある。 つまり、攻撃順序を調整するのが重要なのだ。 更に言えばこのゲームでの攻撃順序は戦闘終了時の状態が次の先頭に持ち越される。 同じ敵が連続して現れても自分の仲間たちの攻撃順序が違ってしまうので同じようには出来ない。 常によく考えることを強制されるので戦闘が単調にならないようになっている。

では、攻撃順序の調整はどうやるのかというと、攻撃の強さで選択する。 弱い攻撃なら次に攻撃するまでの時間は短かく、強い攻撃なら長くなる。 キャラを鍛えるとより強い攻撃を選択できるようになるが、常にめいっぱい強い攻撃をすればよいというわけではないということだ。

ゲームの分類としては、公式ページにはRPGと書いてあるが、レベルを上げて物理で殴るということが通用しない思考力を問われる戦闘システムはパズルゲームとも言えるだろう。

Document ID: 7200f15d5fbe5311d23d741ce63e380f

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