バグじゃなくて仕様です、と言うと悪く聞こえますが、 仕様が無いときもあるんです。いや、その仕様じゃなくて...
比較演算に == と !=
を使っていませんか?
OCaml では基本型以外のデータ構造を表す変数は、基本的に
はそのデータ構造へのポインタであると理解して良いでしょう。
つまり let a = b
などとした場合、
a
は b
が指しているデータ構造への
ポインタの値がそのままコピーされるだけで、そのデータ構造が
いかに巨大であろうと一瞬でコピーされます。
純粋に関数的プログラミングをしているだけであれば、
このコピー方法で全て事足りるはずです。何故ならば、そのような
状況では全ての値が immutable で、一度生成された値が変更される
事は無いからです。しかし mutable な string やら array やらを
使っている場合は少々困ります。
中身も含めてコピーしたい場合には、データ構造毎にコピーの方法が
違うでしょうから、特別に用意しなければいけない事に
なります。Array.copy
や String.copy
がこれにあたります。
しかし、生成された値が同じでも、当然ポインタとしての値が変わって
きてしまいます。
つまりポインタが違う値であっても、中身が同じと言う事が
ありえるので、データ構造の中身が同じかどうかを調べたい時に
はポインタの値が一緒かどうかを調べるだけでは不十分です。
一々自前で中身まで比較する関数を作るのは面倒だし、
自動化できるので OCaml では中身を比較する演算子
(= と <>
) と
ポインタを比較する演算子 (== と !=
)
の両方があります。
中身の比較の場合は、中身が基本型になるまで再帰的に
データ構造の中身を比較して行き、
ポインタ比較は単純にポインタの値だけを比較する事になります。
# type t = { x: int; y: string };;
type t = { x : int; y : string; }
# let a = { x = 3; y = "foo" };;
val a : t = {x = 3; y = "foo"}
# let b = { x = 3; y = "foo" };;
val b : t = {x = 3; y = "foo"}
# a = b, a <> b;; (* 中身比較 *)
- : bool * bool = (true, false)
# a == b, a != b;; (* ポインタ比較 *)
- : bool * bool = (false, true)
ポインタ比較を行なう時にはどういう場合にデータ構造が生成されて いるのかに気を配る必要があります。以下では 4 つの "foo" が 別々のポインタ値を持つデータ構造として生成されている事になります。
# "foo" = "foo", "foo" == "foo";;
- : bool * bool = (true, false)
と言うわけで、大抵の場合は
= と <>
を使うのがおそらく正解ですが、
中身の比較演算子がある事を知らずに、C 言語と同じ
(== と !=
) を使っていると、
なんじゃこりゃって事になります。
参考までに、関数の中身が同じかどうか (どんな引数が来ようが、 同じ結果を返す) は一般には決定不能なので、 中身比較をしようとすると例外が生じます。
ただし、中身比較はまずポインタ比較をしてから中身の比較に入るので、 (ポインタが一緒であれば必ず一緒なので) まったく同じ関数同士の比較はできます。
# let f x = x;;
val f : 'a -> 'a =
# let g x = x;;
val g : 'a -> 'a =
# f == g;;
- : bool = false
# f = f;; (* f と f は等しいのはわかる *)
- : bool = true
# f = g;; (* わからない *)
Exception: Invalid_argument "equal: functional value".
# type t = { mutable hoge: int };;
type t = { mutable hoge : int; }
# let a = Array.make 5 { hoge = 0 };;
val a : t array =
[|{hoge = 0}; {hoge = 0}; {hoge = 0}; {hoge = 0}; {hoge = 0}|]
# a.(2).hoge <- 3;;
- : unit = ()
# a;;
- : t array = [|{hoge = 3}; {hoge = 3}; {hoge = 3}; {hoge = 3}; {hoge = 3}|]
なんじゃーこりゃ?ってなことですが、 要はメモリ確保がどこで行なわれるかがちゃんと理解できていないといけない、 と言う事ですね。Array.make に渡された { hoge = 0 } と書いてあるのは、 C++ を真似て言えば、new t (hoge=0)、の意味になります。 上の例では一回しかそれが行なわれず、配列の要素は全部同じ { hoge = ... } を指していたわけで、 一つ変更すれば同じ物を指しているので全部変更されたかのように見えるわけです。 似たような問題は、上のように mutable なフィールドを持つ record の他に、 配列や文字列を使う時に生じます。 副作用を使う時は気をつけましょう!
以下のようにすれば意図した事ができるでしょう。 Array.init は、要素番号を受けとる関数を用いて配列の中身を初期化してくれる関数です。 (fun i -> { hoge = 0 }) は、i を受け取ってはじめて { hoge = 0 } が評価されるため、 配列の各要素に関して新しい { hoge = 0 } が生成される事になります。
# let a = Array.init 5 (fun i -> { hoge = 0 });;
val a : t array =
[|{hoge = 0}; {hoge = 0}; {hoge = 0}; {hoge = 0}; {hoge = 0}|]
# a.(2).hoge <- 3;;
- : unit = ()
# a;;
- : t array = [|{hoge = 0}; {hoge = 0}; {hoge = 3}; {hoge = 0}; {hoge = 0}|]