Skip to content

Latest commit

 

History

History
328 lines (249 loc) · 16.3 KB

ocaml_i18n.md

File metadata and controls

328 lines (249 loc) · 16.3 KB

OCaml プログラムで欧文以外の文字を使う方法

OCaml ソースコードの文字コード

OCaml のソースコード(ml, mli, mly, mll などの拡張子を持つファイル)の文字コードは リファレンスマニュアルには特に(恐らく)明記されていなません。 しかし OCaml コンパイラのソースコードを見ると欧文の文字コードであるISO-8859-1暗に仮定していることがわかります。 このため、 OCaml ではソースコードにそれ以外の文字コードを指定するための pragma の ようなものはありません。

ISO-8859-1 を仮定している OCaml ですが、ソースコードの文字コードとして EUC-JP や UTF-8 を採用し、日本語や非アルファベット文字などを文字列リテラルやコメントに直接書き込むことは 普通におこなわれています。この文書ではこの仮定と、実際について説明します。

まずはじめに結論

  • 「ASCII部」のエンコードが ISO-8859-1 と同一で、それ以外の文字は 0x80 以上のバイト符号からのみなる EUC や UTF-8 のような文字コードでは文字列やコメント中に ASCII範囲外の文字列を書き込むことが可能。ただし、文字列処理には注意が必要。
  • JIS (ISO-2022-JP他)などは、アルファベットを含むエスケープシーケンスのため使用不可能
  • Shift_JIS は「だめ文字問題」があるため使うことは不可能
  • UTF-16, UTF-32 は ASCII部が ISO-8859-1 と一致しないので使用不可能

識別子に使用できる文字種

OCaml では識別子の名前に使うアルファベットとして ASCII の範囲にあるもの ('a'-'z', 'A'-'Z') だけでなく ISO-8859-1 に登場するすべてのアルファベット('é' などのアクセント記号のついた 文字など)が使用可能です。OCaml プログラムでは識別子の一文字目が小文字であるか、大文字であるかで その識別子の種類が決まりますが、この ASCII 範囲外のアルファベットにもこのルールが適用されます。 ただし、この ASCII 範囲外のアルファベットの識別子としての使用は最近の OCaml のバージョン (4.01.0 から)では推奨されないむね警告が出ます(以下は ISO-8859-1 環境での例。EUC や UTF-8 では再現しません):

ISO-8859-1 環境:

let café = "coffee"     (* 警告 3 が出る *)

ですから現実的には、OCaml の識別子として使用できる文字は ASCII に限られている とみなしてよいでしょう。日本語の文字や特殊記号などを識別子として使うことはできません。

特定の文字コードでの非ASCII文字列のバイト列には、ISO-8859-1 として無理やり解釈すると ISO-8859-1 のアクセントつきアルファベットとして解釈できるものがあります。 (EUC-JP における"珈琲"など。)このような文字コードと文字列を使用すれば、ISO-8859-1 に 含まれない文字であっても無理やり OCaml の識別子として解釈させることは可能です。 が、冗談以上のものではありません。

文字型(char)に使用できる文字種

char は ISO-8859-1 の各文字を表すためのデータ型で、データ幅は 1バイトです。 Charモジュールの lowercase, uppercase, escapedchar が ISO-8859-1 の各文字を表しているとして動作します。

ISO-8859-1 環境:

let e' = 'é'

もちろん、文字コードの仮定を無視して char を単なる 0 から 255 までの1バイト幅のための データとして使うことには何の問題もありません。

ISO-8859-1 環境:

let c240 = char_of_int 240
let () = print_int (int_of_char c240)

ISO-8859-1 以外の文字コードを使ってコードを書いている場合でも、 ISO-8859-1 と一致する 部分(EUC や UTF-8 であればASCIIの範囲)の文字については char を使用することができなくはありません。 ただし、 EUC や UTF-8 などのマルチバイト文字を含むような文字コードではマルチバイトとなる 文字を表記することができませんから、これらのコードで統一的に文字を扱うばあい、 char は あまり役に立たないと思われます。

EUC もしくは UTF-8 環境:

let ko = 'こ'        (* Error: Syntax error *)

文字列(string)に使用できる文字種

string は ISO-8859-1 の文字列を表すためのバイト列を表すデータ型です。 string内の各バイトが ISO-8859-1 の文字を一つ表します。 String モジュールの escaped, uppercase, lowercase, capitalize, uncapitalize といった関数も ISO-8859-1 の符号列として string を処理します。

ISO-8859-1 環境:

let coffee = "café"
let () = print_endline (String.uppercase coffee)  (* CAFÉ と出力される *)

もちろん、文字コードの仮定を無視して string を単なるバイト列のデータとして 使うことも可能です。

ソースコードを EUC や UTF-8 とみなした場合、これらの文字コードの文字列を stringリテラルに直接埋め込むことは可能で、実際に良く行なわれています。 OCaml はこれらのバイト列を ISO-8859-1 として解釈しますが、間違って文字列のパースに 影響を及ぼすような文字エンコードは無いため問題はおこりません。

EUC もしくは UTF-8 環境:

let hello = "こんにちは"
let () = print_endline hello          (* 標準出力に こんにちは と出力される *)

ただし、上記の大文字小文字変換などの ISO-8859-1 を仮定する関数を使うと意図しない結果となります。 また、substring など文字列の位置や長さを取る関数をマルチバイト文字を含む文字列の処理をする場合 には ISO-8859-1 としての文字長(つまりバイト数)と使用している文字コードでの文字数との間の変換に 注意が必要です。

EUC もしくは UTF-8 環境:

let hello = "hello こんにちは"
let () = print_endline (String.uppercase hello)    (* こんにちは が ISO-8859-1 として大文字に変換されしまい、
                                                      出力は HELLO Á\223Â\223Á\253Á\241Á\257 
                                                      となってしまう *)

なお、 Shift_JIS は「だめ文字問題」 があるため文字列中であっても使用は不可能です。

コメントに使用できる文字種

OCaml はコメント (* .. *) の文字列も内部では ISO-8859-1 として解釈しています。 文字列と同じく、プログラムの文字コードに EUC や UTF-8 を選択した場合、コメント中に これらの文字列を書くことが可能です。

EUC もしくは UTF-8 環境:

(* コメントを日本語で書いてみたぞ *)

Shift_JIS はやはり「だめ文字問題」があるためコメントに日本語を書くとトラブルの元になります。

OCaml toplevel(REPL) での問題

OCaml toplevel (REPL) は、対話的にプログラムテキストを受け取り、結果を表示する以外は、 基本的に OCaml コンパイラと内部は同じです。ですから toplevel 中での文字コードの使い方も 上記と変わりません。ただし、いくつか注意点があります。

正しい端末設定を使用する

OCaml を使う以前の問題ですが、使用したいと思っている文字コードと実際の端末の文字コードは 一致していなければいけません。自分は UTF-8 を使用しているつもりでも、端末は Shift_JIS であったという場合、意味不明な挙動に苦しめられることになるでしょう。

ISO-8859-1 で特殊文字とみなされる文字はエスケープして表示される

「OCaml で文字化けした」と言われる問題は全てこれだと思って間違いありません。

UTF-8 環境の toplevel:

# "こんにちは";;
- : string = "\343\129\223\343\129\253\343\129\241\343\129\257"
# 

こんな感じに「文字化け」します。(正確にどのような出力になるかは端末に依存します。)

これは OCaml が文字列をあくまで ISO-8859-1 として処理しているからです。 例えば 0x83 というバイトは ISO-8859-1 で表示する文字を持ちません、 そのため OCaml はこのバイトをエスケープして "\203" という文字列として表示します。 (203 は 0x83 の 8進数です。)この ISO-8859-1 を仮定したエスケープのため、 ISO-8859-1 以外の文字コードを使用している場合、 0x80 以上のバイトを含む文字は OCaml toplevel では「文字化け」して表示されてしまいます。

これは入力側で問題が発生しているわけではなく、純粋に toplevel の文字列の出力方法 によるものです。Toplevel の文字列出力を使わず直接標準出力に同じ文字列を書き出すと EUC や UTF-8 の文字列は内部では正確に保持されており、壊れているわけではないことが わかります。

EUC もしくは UTF-8 環境:

# print_endline "こんにちは";;
こんにちは
- : unit = ()
# 

Toplevel (REPL) での文字化けを防ぐ

内部的には壊れているわけではない EUC や UTF-8 の文字列ですが、出力が狂ってしまうのは不便です。 これは OCaml の string プリンタを変更することで回避することができます:

EUC もしくは UTF-8 環境:

# let print_non_escaped_string ppf = Format.fprintf ppf "\"%s\"";;
val print_non_escaped_string : Format.formatter -> string -> unit = <fun>
# #install_printer print_non_escaped_string;;
# "こんにちは";;
- : string = "こんにちは"

ここでは 文字列をエスケープせずに標準出力に出力する関数、 print_non_escaped_string を定義し、 それを #install_printer ディレクティヴによって string 型のプリンタに指定しています。 これにより文字列を ISO-8859-1 とみなしたエスケープが行なわれなくなります。

Toplevel で日本語を含んだ文字列などを多用する場合は、毎回この内容を打ち込むのをさけるために、 次の内容を OCaml toplevel が起動時に実行するファイルである .ocamlinit に 書き込んでおくとよいでしょう:

let print_non_escaped_string ppf = Format.fprintf ppf "\"%s\"";;
#install_printer print_non_escaped_string;;

(OCaml toplevel は .ocamlinit ファイルをまずカレントディレクトリで探し、 なければ $HOME/.ocamlinit を探します。ocaml -init <file> で指定することも可能です。)

なお、このプリンタ、 print_non_escaped_string は簡易的なものであって、 EUC や UTF-8 で本当にエスケープするべき文字のエスケープは全く行いません。きちんとした エスケープを行いたい場合はそれなりの関数を書く必要があります。 私は興味が無いので知りません。

OCaml で ISO-8859-1 以外の文字列データを処理する

前節では、 OCaml のプログラミングソースには ISO-8859-1 が仮定されていること、 しかし、 EUC や UTF-8 の文字コードを選択することは、文字列を扱う関数の挙動は別として 問題ない、ということを見ました。

ここからは、実際にこれらの文字コードを選択してどのようにマルチバイト文字を含む文字列を 処理するかについて議論します。

方法1: 特にライブラリなどを使わず済ませる

複雑な文字列処理を行わない場合、string型のデータをそのまま気にせず String モジュールの関数で処理しても問題無い場合が多々あります。 ISO-8859-1、EUC、UTF-8 の表現はASCIIの範囲内に限っては同じですから マルチバイト文字部分を直接いじらない処理の場合は上手くいく事が多いのです。 例えば、次のような , で区切ってあるデータを切り出して列挙要素を取り出す プログラムはこの方法で十分な場合があります:

let split_by_comma s =
  let rec loop pos = 
    try 
      let pos' = String.index_from s pos ',' in
      String.sub s pos (pos' - pos) 
      :: loop (pos'+1)
    with
    | _ -> [ String.sub s pos (String.length s - pos)]
  in
  loop 0

let () = 
  let tokens = split_by_comma "  名前 ,  クラス,  体重,身長  \n" in
  List.iter (fun t ->
    (* もちろん String.trim は「全角スペース」には対処できません *)
    Printf.printf "\"%s\"\n" (String.trim t)) tokens

String モジュールの関数群は文字コードとしてあくまでも ISO-8859-1 を 前提としています。ですから、大文字小文字の判定や変換を伴なうものは誤動作しますから使えません。 また、lengthindexsub のような関数は EUC や UTF-8 での文字数では 動作せず、ISO-8859-1 として見た時の文字数、つまりバイト数を使用することに注意を払う 必要があります。

方法2: 自分でマルチバイト部分の処理を書く

EUC や UTF-8 であっても、文字の意味的処理(大文字、小文字であるかなど)を行わず、 単に文字の切出しくらいの処理であれば、データ型を string のままにして、 自力で文字カウントのスキャンを書くことは可能ですし、選択肢として考えるべきでしょう。

これが大掛りになってくると、選択した文字コードとして正しいバイト列持つ string を 内部表現とする抽象データ型を作るなど、ライブラリ化が始まります。

module UTF8 : sig

  type t (** UTF-8 文字列 *)
  type c = int (** Unicode 文字コードポイント *)

  exception Invalid

  val of_bytes : string -> t
  (** バイト列を UTF-8 とみなして検査を行う。不正なバイト列を含む場合、
      Invalid 例外を発生する *)

  val to_bytes : t -> string
  (** [t] のバイト列を返す *)

  val get_char : t -> int -> c
  (** [get_char t n] は [n] 文字目のコードポイントを返す。 O(n) *)

  val length : t -> int
  (** [length t] は [t] の文字数を返す *)

  val bytes : t -> int
  (** [bytes t] は [t] のバイト数。 O(n) *)

end = struct

  type t = string (* 内部は string *)

  let is_valid s = ... (* 自分で書いてね *)
    
  let of_bytes s = if is_valid s then s else raise Invalid

  let to_bytes t = t

  let get_char t n = ... (* 自分で書いてね *)

  let length t = ... (* 自分で書いてね *)

  let bytes = String.length
end

このような形になってしまって、特に可変バイト長の文字コードでの文字の取扱いを 効率的に行いたいとなると…自作でライブラリを構築するよりはサードパーティの ライブラリを使う方が簡単になってきます。

方法3: Unicode ライブラリを使用する