処理系:日本語の扱い - lisp-cookbook-ja/common-lisp GitHub Wiki
言語規格で決められている多言語化の仕組みと、処理系ごとの日本語への対応状況について説明します。Common Lispの型の仕組み、文字集合、文字符号化方式(以下エンコーディング)について、最低限の知識があることを前提にしています。
多言語化の仕組み
ANSI Common Lisp(以下ANSI)では、色々な言語を扱うためにどのような仕組みを用意しているかについて説明します。特に断らない限りはANSIに準拠している処理系に共通の内容です。
文字
ANSIでは、標準の96文字以外、どのような文字が使えるかは、処理系が自由に決めて良いことになっています。多くの処理系ではUnicodeのすべての文字が使えるようになっているので、現代の日本語を扱う分にはあまり困ることはありません。
文字型
文字を扱うときは、文字に対応する文字型のオブジェクトを使います。また、文字型には種類があり、対応するオブジェクトがどの型か、文字によって違いがあります。
文字型のオブジェクトの例
オブジェクト 対応する文字
======================================================================
#\a a
#\あ あ
:character:すべての文字型の基底型です。処理系がサポートするすべての文字のオブジェクトはcharacter型です。どの文字をサポートするかは処理系が自由に決められます。文字の数にも上限はありません。 :standard-char:base-char型の派生型です。改行、スペース、ラテン文字の基本字(俗に言うアルファベット)、アラビア数字、32種類の記号の、合わせて96文字のオブジェクトはstandard-char型です。ANSIで、処理系は必ずこの96文字をサポートしなければいけないと決められています。 :base-char:character型の派生型で、standard-char型の基底型です。処理系がサポートする文字のうち、どれだけの文字のオブジェクトがbase-char型なのかは、処理系が自由に決めて良いことになっています。ただし、standard-char型である96文字のオブジェクトは、base-char型でなければなりません。 :extended-char:character型の派生型です。character型のオブジェクトなのにbase-char型でないものは、すべてextended-char型です。
文字型の関係
┏━━━━ character ━━━━┓
┃┏━━━ base-char ━━━┓┃
┃┃┏━ standard-char ━┓┃┃
┃┃┃ a b c ... ┃┃┃
┃┃┗━━━━━━━━━━┛┃┃
┃┗━━━━━━━━━━━━┛┃
┃ extended-char ┃
┗━━━━━━━━━━━━━━┛
日本語の平仮名や片仮名、漢字などの文字に対応するオブジェクトは、base-char型かextended-char型のどちらかに属しますが、どちらに属するかは処理系によって違います。また、base-char型もextended-char型もcharacter型の一種なので、それらのオブジェクトはcharacter型のオブジェクトでもあります。
;;; Clozure CL 1.8
(type-of #\a)
;=> STANDARD-CHAR
(typep #\a 'base-char)
;=> T
(type-of #\あ)
;=> CHARACTER
(typep #\あ 'standard-char)
;=> NIL
(typep #\あ 'base-char)
;=> T
(typep #\あ 'extended-char)
;=> NIL
文字のデータ表現
ANSIに準拠した処理系の中の世界では、文字の違いをデータとして表す方法、いわゆるエンコーディングは一種類しかありません。ANSIでは、文字に対応するオブジェクトが、それぞれの文字の違いをデータとしてどう表現するかは決まっていませんので、処理系の開発者が自由に選択・設計できます。
処理系の中でのデータ表現はひとつしかないので、処理系の外から与えられるデータを処理する場合、データを正しく解釈させるために、データ表現を処理系の中のものと合わせなければなりません。
逆に、処理系の中から外にデータを与える場合も、処理系の外のデータ表現に合わせないと、データが誤った解釈をされてしまいます。
UTF-16LEで表現された「日本語」
┏━━━━━┳━━━━━┳━━━━━┓
┃ 日 ┃ 本 ┃ 語 ┃
┣━━┳━━╋━━┳━━╋━━┳━━┫
┃0xe5┃0x65┃0x2c┃0x67┃0x9e┃0x8a┃
┗━━┻━━┻━━┻━━┻━━┻━━┛
同じデータのCP932での解釈
┏━━━━━┳━━┳━━┳━━━━━┓
┃ 蘰 ┃ , ┃ g ┃ 條 ┃
┣━━┳━━╋━━╋━━╋━━┳━━┫
┃0xe5┃0x65┃0x2c┃0x67┃0x9e┃0x8a┃
┗━━┻━━┻━━┻━━┻━━┻━━┛
CP932で表現された「日本語」
┏━━━━━┳━━━━━┳━━━━━┓
┃ 日 ┃ 本 ┃ 語 ┃
┣━━┳━━╋━━┳━━╋━━┳━━┫
┃0x93┃0xfa┃0x96┃0x7b┃0x8c┃0xea┃
┗━━┻━━┻━━┻━━┻━━┻━━┛
ANSIに準拠した処理系では、この処理系の内部と外部とでのデータ表現、エンコーディングの変換によって、様々なエンコーディングに対応しつつ、処理系の中での一貫した文字の処理を実現しています。この方法はUCS Normalizationと呼ばれ、Java、Perl、Python、.NET、Windows、Mac OS Xなどで広く採用されています。
文字列
ANSIでの文字列とは、文字型のオブジェクトの一次元配列(ベクタ)です。これはANSIでも明確に決められていて、配列やベクタ用の操作を文字列に対しても使うことができますし、サイズの制限など、'''配列の特性もそのまま受け継ぎます。'''
文字と配列の要素が一対一で対応する、メモリ上のデータ表現とは切り離された設計になっているため、バイト列としてマルチバイト文字を扱うときのような複雑さとは無縁です。
文字列の例
"abc"
"あいうえお"
文字列型
:string:character型を要素に持つvector型です。 :base-string:base-char型を要素に持つvector型です。standard-char型の96文字からなる文字列を効率良く扱えることになっています。 :simple-string:character型を要素に持つsimple-array型です。処理系によってはstring型より効率良く扱える場合があります。 :simple-base-string:base-char型を要素に持つsimple-array型です。処理系によってはbase-string型より効率良く扱える場合があります。
;;; CLISP
(type-of "a")
;=> (SIMPLE-BASE-STRING 1)
(type-of "あ")
;=> (SIMPLE-BASE-STRING 1)
external format
実際に処理系が外の世界とデータをやり取りするとき、エンコーディングの変換をする関数に、どのエンコーディングへと変換するのか、どのエンコーディングから変換するのかを指定するためのオブジェクトです。具体的にどのようなオブジェクトであるべきか、どのようにオブジェクトを作るべきかはANSIで決められていないので、処理系によって差があります。例えば、CLISP 2.49ではビルトインの型のオブジェクトですし、Clozure CL 1.8ではCommon Lispの構造体です。
;;; external formatオブジェクトを作るAPIの違い
;;; 実際にexternal formatオブジェクトが必要とされる場面では、定数やdesignatorを
;;; 使うため、自分でオブジェクトを作る機会はほとんどない
;; CLISP 2.49
(ext:make-encoding)
;=> #<ENCODING "CP932" :DOS>
;; Clozure CL 1.8
(ccl:make-external-format)
;=> #<EXTERNAL-FORMAT :ISO-8859-1/:UNIX #x183D9E36>
余談ですが、改行コードもエンコーディングに事情が似ていて、OSなどの環境や、HTTPなどのプロトコルの規格によって適切な値が変わります。そのため、多くの処理系では改行コードの情報もexternal formatオブジェクトで一緒に扱えるようにして、エンコーディングと同時に改行コードも変換できるように拡張しています。
上のコードでは、評価例の部分で「:DOS」や「:UNIX」のように表されているのが改行コードの情報です。:DOSは現在では主にWindowsで使われているCRLF、:UNIXは主にUNIX系のOSで使われているLFを表しています。
エンコーディングの変換
エンコーディングの変換についてANSIで決められているのは、ファイルに収められたデータについてだけです。同じファイルに関するものでも、ファイル名については特に決められていません。
load、compile-file、openにはexternal-formatというキーワード引数があり、これにexternal formatオブジェクトや対応するキーワードなどを渡すと、ファイルを読み込む過程で、必要に応じて自動的にエンコーディングを変換してくれます。
;;; CLISP 2.49
;; charset:utf-16はexternal formatオブジェクトの定数
(with-open-file (s "utf-16.txt" :external-format charset:utf-16)
(read-line s))
;=> "UTF-16でエンコードされたファイル", NIL
;;; Clozure CL 1.8
;; external format desinatorとしてキーワードシンボルの:utf-16を指定
;; ccl::normalize-external-formatで正規化される
(with-open-file (s "utf-16.txt" :external-format :utf-16)
(read-line s))
;=> "UTF-16でエンコードされたファイル", NIL
多くの処理系では、ネットワークやプロセス間通信でも同じ形でエンコーディングを指定できるようにしています。
;; CLISP 2.49
(with-open-stream
(s (ext:make-pipe-input-stream "env LANG=ja_JP.UTF-8 date"
:external-format charset:utf-8))
(read-line s))
;=> "2012年 7月 14日 土曜日 13:45:59 JST", NIL
;; Clozure CL 1.8
(with-output-to-string (s)
(ccl:run-program "env"
'("LANG=ja_JP.UTF-8" "date")
:output s
:external-format :utf-8))
;=> "2012年 7月 14日 土曜日 13:47:17
; "
また、処理系によっては、8ビットの符号なし整数を要素に持つベクタと、文字列とをお互いに変換する関数が用意されていることがあります。これを利用することで、テキストデータをバイト列として処理することもできます。
;;; CLISP
(ext:convert-string-to-bytes "日本語" charset:utf-8)
;=> #(230 151 165 230 156 172 232 170 158)
(ext:convert-string-from-bytes #(230 151 165 230 156 172 232 170 158)
charset:utf-8)
;=> "日本語"
;;; Clozure CL
(ccl:encode-string-to-octets "日本語" :external-format :utf-8)
;=> #(230 151 165 230 156 172 232 170 158), 9
(let ((v (make-array 9
:element-type '(unsigned-byte 8)
:initial-contents '(230 151 165 230 156 172 232 170 158))))
(ccl:decode-string-from-octets v :external-format :utf-8))
;=> "日本語", 9
データの処理
前節で説明した枠組みをそれぞれの処理系がどのように仕上げ、データとしての日本語をどの程度扱えるのかを紹介します。
処理系が対応しているエンコーディングは、代表的なものと日本語に関係するものだけ抜粋しています。また、処理系内部で使われているエンコーディングを強調しています。
CLISP
Implementation Notes for GNU CLISPの31.5. Encodingsで説明されています。
デフォルトの内部エンコーディングはUCS-4です。configure時に--without-unicodeを指定してビルドすると、代わりにISO/IEC 8859-1が使われるようになります。また、$$wp glibcかGNU libiconvをリンクすることで、より多くのエンコーディングをサポートします。
また、処理系の外部とデータをやり取りするとき、特に指定がなかった場合に解釈に使われるエンコーディングは、UNIX系のOSで処理系を動作させている場合はロケール、Windowsの場合ではコードページの情報を元にして自動的に設定されます。システム標準のエンコーディングを使う限り、ユーザが手動で設定をする必要はないでしょう。
対応するエンコーディング
- US-ASCII
- '''ISO/IEC 8859-1'''
- UCS-2
- '''UCS-4(UTF-32)'''
- UTF-8
- UTF-16
- ISO-2022-JP
- Shift_JIS
- CP932
- EUC-JP
文字型が扱う文字集合
:character, base-char:ISO/IEC 10646
ファイル名
パスネームをcustom:pathname-encodingで指定したエンコーディングに変換してアクセスします。
日本語版Windowsでは日本語のファイル名の扱いに制限があります。CLISPではANSI系のWindows APIを利用しているため、日本語版Windowsでは通常、CP932でエンコードされたファイル名をAPIに渡さなければなりません。しかし、CP932でエンコードされたデータには0x5cが含まれることがあるという有名な問題があり、CLISPがファイル名のエンコーディングを変換するとき、この0x5cを不正な文字とみなしてしまうため、一部の文字をファイル名として使えません。
この制限を回避するひとつの方法として、Cygwin 1.7以降でCygwin向けのCLISPを使うという手があります。
1.7以降のCygwinは、Windowsのコードページとは別に、UTF-8とWindows内部のエンコーディング(UTF-16LE)の変換層を持っています。CygwinのAPIにUTF-8でエンコードされた文字列を渡すと、CygwinのAPIの中で呼ばれるUnicode系のWindows APIに、UTF-16LEでエンコードされた文字列を渡してくれます。これを利用すれば、CP932による制限を回避できます。その場合、custom:pathname-encodingはcharset:utf-8にしてください。
ただ、Cygwin向けのCLISPでは、Windows方式のパスが使えない(例えば、c:\CLISP\clisp.exeの代わりに/cygdrive/c/CLISP/clisp.exeと指定しなければなりません)、互換層による性能の低下、といった別の問題もあります。利点と欠点を比べて、どうするかを決めてください。
Clozure CL
Clozure CL Documentationの4.5. Unicodeで説明されています。
単純さと速度の面から、内部エンコーディングにはUTF-32が採用されています。CP932とEUC-JPはサポートされていますが、ISO-2022-JPはサポートされていません。
対応するエンコーディング
- US-ASCII
- ISO/IEC 8859-1
- UCS-2
- UTF-8
- UTF-16
- '''UTF-32'''
- CP932
- EUC-JP
文字型が扱う文字集合
:character, base-char:Unicode
CMU Common Lisp
CMUCL User's ManualのInternationalizationで説明されています。
メモリ上でのサイズや複雑さとのバランスを取って、内部エンコーディングにUTF-16を採用しています。対応するエンコーディングが限られるので、Unicode以外を扱う場合は変換ライブラリを利用する必要があります。
対応するエンコーディング
- ISO/IEC 8859-1
- UTF-8
- '''UTF-16'''
- UTF-32
文字型が扱う文字集合
:character, base-char:Unicode
Steel Bank Common Lisp
SBCL Internalsの9 Character and String Typesで説明されています。
文字型はヒープにアロケートされない即値で、内部エンコーディングは21ビットのUnicodeのコードポイントにタグ付けしたものです。:SB-UNICODEがfeaturesに含まれない場合、内部エンコーディングはISO/IEC 8859-1になります。Shift_JISとEUC-JPはサポートされていますが、ISO-2022-JPはサポートされていません。
対応するエンコーディング
- US-ASCII
- '''ISO/IEC 8859-1'''
- UCS-2
- UCS-4
- UTF-8
- UTF-16
- UTF-32
- Shift_JIS
- EUC-JP
文字型が扱う文字集合
:base-char:UnicodeのBasic Latin(U+0000-007F) :character:Unicode
ユーザーインターフェイス
ここまでの内容と少し離れて、ANSIで決められていない、処理系のユーザーインターフェイスでの日本語の扱いについて紹介します。
CLISP
GNU gettextに対応していますが、日本語のメッセージカタログは用意されていません。日本語のメッセージカタログを用意すれば、日本語でメッセージを表示させられます。
Clozure CL
メッセージは英語固定です。国際化の仕組みは今のところありません。
参考資料
- Common Lisp と 日本語 と 文字コード
- 文字コード - Wikipedia
- Common Lisp HyperSpec
- Implementation Notes for GNU CLISP
- src/encoding.d - CLISPのソースコードから
- src/lispbibl.d - CLISPのソースコードから
- Clozure CL Documentation
- level-1/l1-sysio.lisp - Clozure CLのソースコードから
- CMUCL User's Manual
- SBCL Internals
- Rubyist Magazine - Ruby M17N の設計と実装