Perl の内部形式に関する考察

もともとこの記事は daily dayflower さんとこの UTF8 フラグあれこれ の記事を見て、内部表象 (内部形式: internal format) について自分が持っている認識と違う部分があったため自分なりの考察を書いていました。その考察に対して Perl (5.8) での文字列の内部表象について返信 にて誤りの指摘をして頂いたので(ありがとうございます)改めて今現在の自分が持っている認識を書いておきます。(2008.03.13)

なお、Perl (5.8) での文字列の内部表象について返信 で引用して頂いた元々の文書は、こちら に移動しました。内容は誤っているため参考にしないようにしてください。

はじめに

重要なこととして、Perl 5.8 以降では、文字列 (text strings) とバイナリ列 (binary strings) は区別されるということがあります。バイナリとしてのデータはバイナリ列、文字列は文字列です。

文字、例えば「あ」という文字は「あ」という文字以外の何者でもありません。ですが、コンピュータ上で「あ」という文字が直接扱えるわけではない(コンピュータはデータをビット列として扱う)ので、文字をコンピュータが扱える形(バイナリデータ)にしなければありません。それがエンコードとよばれるもので様々なエンコード方式があります。同じ「あ」という文字でもエンコード方式によって異なるバイナリデータになってしまいます。「あ」という文字をエンコードすると

となります。

このように、同じ文字を表しているのにデータ(バイナリ)としては違うものになってしまっては、「文字」として扱うのが大変になります。そのため、Perl 5.8 では「文字」を「文字」として扱える機能が追加されました。(正確には Perl 5.6 からあったのですが、バグが多かったようです。)

Perl が扱う文字は Unicode と呼ばれる文字集合です。(ひらがなの 50 音表のようなもの。)Unicode の文字は、文字の並び順に対応する数字 (コードポイント) によって文字を表します。「あ」は Unicode の 12354 番目の文字なので、コードポイントは 12345 (0x3042) になります。"U+" に続けてコードポイントの 16 進数値を書くことで、その文字を表現することになっています。(つまり U+3042 は「あ」を表す。)
Perl で Unicode 文字をコードポイントで表現する場合は、\x{コードポイントの 16 進数値} という表現になります。

文字列 (text strings) とバイナリ列 (binary strings) の違いについては perlunitut が詳しいので参照してください。私が和訳した分はこちらです。

内部形式 internal format

文字を文字として扱うと言っても、先に述べたように、コンピュータは直接「文字」を扱えるわけではないので、Perl は、内部的にはバイナリデータとして文字列を扱います。この内部的なバイナリデータを内部形式 (internal format) と言います。

内部形式は Perl の実行環境に依存し、ascii / Latin-1 環境では UTF-8 を、EBCDIC 環境では UTF-EBCDIC を使用します。perldoc ではこれらをまとめて UTF-X と表現しています。が、内部形式がどんなエンコード形式なのか考える必要はなく、ただ、どの「文字」を表しているかのみを考えるべきです。

ここで UTF8 フラグあれこれ を見てみましょう。まとめの部分に、スカラ変数に格納できる値として

とあります。つい先ほど「文字列」とは「文字列」であり内部形式(内部表象)を考えるべきでないと言いましたが、ここでは文字列に (A) と (B) の 2 種類があります。これがどういう事か考えてみます。

「バイナリ列」を文字列として評価した場合

Perl 5.8 以降の正式な「文字列」というのは、(A) の方です。
しかし、Perl が Unicode に対応する以前は、octet stream を「文字」として扱っていました。そこで互換性のために、バイナリ列を「文字列」(文字リテラル)として評価する時には、「Latin-1 エンコードされた文字」として評価するのです。例えば、C5 というバイトは、Latin-1 エンコードでは 'Å' という文字を表すので、C5 = 'Å' となります。これが (B) の「文字列」の意味するところです。(ascii / Latin-1 環境の場合。)

dayflower さんのところでも同様の例がありますが、以下に例を示しておきます。以下では「バイナリ列」であることを明確に示すために pack していますが \xXX の形式で表現しても同様です。

my $textStr = "ÅÆ"; # utf8 encoding
utf8::decode($textStr);
# $textStr は "ÅÆ" という「文字列」を表す.
# "ÅÆ" は Latin-1 エンコードでは \xC5 \xC6 で表される.

my $binaryStr = pack("n", 0xC5C6);
# $binaryStr はバイナリ構造体.
# 0xC5C6 という数値を big-endian の unsigned short (16 bits) に pack.

print $textStr eq $binaryStr ? 1:0, "\n";
# => 1
# 文字リテラルで $binaryStr を評価すると、
# Latin-1 エンコードされた文字とみなすので、
# "ÅÆ" であるとみなされる.
# よって、$textStr と $binaryStr は文字として等しい.

他にも例を挙げておきます。pack / unpack 関数の U テンプレートは、「文字」と「Unicode コードポイント」を仲介します。つまり、「文字」$char に対して unpack('U', $char) すると、$char の Unicode コードポイントを返します。バイナリデータに対して同様にテンプレート U で unpack するとどうなるでしょう? やはり Latin-1 エンコードされた文字とみなされ、その文字の Unicode コードポイントが返ってきます。

my $textStr = "ÅÆ"; # utf8 encoding
utf8::decode($textStr);
# "ÅÆ" (U+00C5 U+00C6) という「文字列」を表す.
printf "%X-%X\n", unpack("U2", $textStr);
# => C5-C6
# 文字列の Unicode コードポイントが返される.

my $binaryStr = pack("n", 0xC5C6);
# バイナリ構造体.
# \xC5 \xC6 というバイナリ列は Latin-1 エンコードで "ÅÆ" を表す.
printf "%X-%X\n", unpack("U2", $binaryStr);
# => C5-C6
# Latin-1 エンコードされた文字とみなし、その文字の Unicode コードポイントが返される.

ここで重要なことは、「バイナリ列」を文字列として評価すると、Perl は Latin-1 エンコードされた文字とみなす、ということです。(ascii / Latin-1 環境の場合。)
EBCDIC 環境では「バイナリ列」を EBCDIC エンコードされた文字とみなすと思われます。

「文字列」をバイナリ列として評価した場合

次に「文字列」をバイナリ列として評価した場合を考えてみます。

ほとんどの pack / unpack 関数のテンプレートは、バイナリ列に対して使うことを想定しています。(そもそも pack / unpack 関数はバイナリ構造体を扱う関数ですから。)例えばテンプレート C や H で文字を unpack するとどうなるでしょうか。Perl 5.8 では、内部形式 (UTF-X) のコードに対して unpack したものが返ってきていました。

Perl 5.10 では pack / unpack の仕様が変わり、普通に使うとその文字のコードポイントに対して unpack したものが返ってきます。また、C0 スイッチ、U0 スイッチというものが追加され、U0 モードでは Perl 5.8 と同様の動作になります。

このように文字列をバイナリとして評価すると内部形式が関係してきます。先に述べたように内部形式というのは気にするべきではありません。よって、内部形式の値で動作するようなプログラムは書くべきではありません。文字に対する pack / unpack は基本的にテンプレート U のみを使うべきでしょう。(W というのもありますが)

他の関数にしても、文字を文字として扱わないものは使うべきではないでしょう。「文字」は Unicode コードポイントとのみ結び付けられるべきです。

まとめ

文字列とバイナリ列は異なる
Perl 5.8 以降では、文字列 (text strings) とバイナリ列 (binary strings) は区別される。
utf8 フラグが付いていれば文字列。さもなければバイナリ列。
バイナリ列を文字列として評価
Latin-1 (ISO-8859-1) の範囲内に収まる文字は、デフォルトでは utf8 フラグが付かない。
chr() しかり、テンプレート W の pack しかり。
バイナリ列を文字列として評価すると、そのバイナリ列を「Latin-1 エンコードされた文字」として評価する。
これは互換性確保などの理由によるもので、動作としては適切なものである。
「文字」として扱う場合、utf8 が付いていようがいよまいが(同じ文字を表すならば)関係がない。
文字列をバイナリ列として評価
文字列をバイナリ列として評価することは、すべきではない。

個人的メモ

PerlIO レイヤ :bytes について。PerlIO レイヤが実装されていないバージョンでは :bytes は擬似レイヤとして働き、ファイル読み書き時の "binary mode" を指定する。その反対に "text mode" を指定するのが :crlf 。
(参考: perldoc - open)

PerlIO が実装されているバージョンでは、:bytes レイヤは :utf8 の逆である。:utf8 レイヤは実際のところ UTF-X (UTF-8 or UTF-EBCDIC) を指定するので、UTF-8 エンコードで IO レイヤを設定したければ :encodeing(utf8) を使うべき。
(参考: perldoc - Perl I/O)

関連文書

UTF8 フラグあれこれ, Perl (5.8) での文字列の内部表象について返信
perlunitut (和訳), perlunifaq, perlunicode, perluniintro, Encode