2017-04-26

文字列とコーデック

R6RSに於いてポートはバイナリと文字列を明確に区別している。例えば、バイナリポートに対して文字列の書き込みはできない。この2つの橋渡し役がトランスコーダになる。トランスコーダはコーデックを受け取り生成され、transcoded-port等の手続きでバイナリポートを文字列ポートに変換する。ここまでは特に問題ないだろう。

さて、ここからが問題である。以下のコードは何を出力するだろうか?
(display "\xFF;\xD8;")
期待する挙動としては"\xFF;\xD8;"がそのまま出力される、つまり0xFF 0xD8として2バイト出力されることを期待するだろうか?

上記の挙動を期待した人は手をあげなさい。
(・ω・)ノシ
手を上げた人のそのまま残りなさい、補習があります。挙げなかった人はこのまま帰ってもよいです。もちろん補習を受けてもよいですよ。

さて、実際の挙動を見てみよう。大抵の処理系では出力される文字列は"ÿØ"となり、これのバイナリ表現はUTF-8で0xC3 0xBF 0xC3 0x98になるだろう。勘がいい方は気づいたかもしれないが、この挙動の正体はトランスコーダの仕業である。displayに出力ポートを指定しなかった場合current-output-portが使われるのは周知のことであると思われる。current-output-portにはどんな文字列ポートが割り当てられているのだろうか?R6RSによると以下である。
These return default textual ports for regular output and error output. Normally, these default ports are associated with standard output, and standard error, respectively. (omit) A port returned by one of these procedures may or may not have an associated transcoder; if it does, the transcoder is implementation-dependent.
要約:規定の出力ポート。通常は標準出力に割り当てられる。返されるポートにはトランスコーダが紐つけられているかもしれない。もしそうならそれは処理系依存である
処理系依存である。例えばSagittariusでは(native-transcoder)が割り当てられるし、Chezはトランスコーダを割り当てていない。

処理系依存の挙動ではいまいち納得が行かないので、これを処理系非依存の挙動で書いてみる。
(call-with-bytevector-output-port
 (lambda (out) (put-string out "\xFF;\xD8;"))
 (make-transcoder (utf-8-codec)))
このコードを実行すると上記と同様の出力が得られる。文字#\xFF#\ÿなるのかというのはUCS4とUTF-8の変換表を見てもらいたい。

ではどうすれば変換せずに出力できるのか?答えは割と簡単でlatin-1-codecを使うと良い。上記のコードを以下のように書き直すと予定通りに動く:
(call-with-bytevector-output-port
 (lambda (out) (put-string out "\xFF;\xD8;"))
 (make-transcoder (latin-1-codec)))
もちろん、文字列の中に多バイト文字が混じっていた場合はエラーになるので気をつける必要があるが。

余談ではあるが、この問題を回避するポータブルな方法はR7RSにはない。つまり、文字列をバイナリ表現として扱う非処理系依存なコードは現状のR7RSでは書けないということになる。また、少なくともChibiとGaucheでは文字列"\xFF;\xD8;"displayで出力したら"ÿØ"が出力された。R7RS-largeに期待したい類の問題である。