静的型と OO というものははじめから…

OO の方面から、「静的型とか別に役に立つとは思えない、静的型の人は頭おかしい」

とか関数型の方面から、「静的型が役に立たないなんてはずない OO の人は頭おかしい」

とか良く聞こえてくるんですが、ダックタイピング心理学 とかいう真に頭おかしい意見を無視できるとすると(無視できない量あるんですが)、まあ私にはどっちもわからんでもない、という話です。

型をゴミ箱に捨てておいてから、後でゴミ箱を漁るなら、型なんかいらない

オブジェクトの静的型システムを大雑把にいうとまず upcast と downcast があります。 upcast はオブジェクトの静的型をそれが属するクラスからそのスーパークラスにを変えちまうこと、downcast はその逆、オブジェクトの静的型をそれが属するクラスから子クラスに変えちまうことです。サブクラスの物はスーパークラスとしても通用するはずですから upcast は失敗しませんが downcast は失敗する可能性があります。そのために downcast 時には本当にそのオブジェクトはサブクラスの要請する条件を満たしているのかを検査しなくてはいけません。

ここで注意して欲しいのは upcast がオブジェクトの静的型情報を積極的に忘れる行為だということです。そして失われた静的型情報は downcast 時のテストによって回復させるのだが、それは失敗する可能性がある。せっかく判っていた型情報をゴミ箱に捨てておいてから、プログラム実行時にゴミ箱を漁っているわけですがこれは勿体無い!

しかし残念ながら、静的に判っている型情報を一部捨てることで柔軟なオブジェクト操作が行える…これが(静的型のある)オブジェクト指向の本質なのです。

言語の本質として upcast が必須な静的型付きオブジェクト指向では静的型を忘れるプリミティブが自由に使えます。ですから型の利点がわかっていないヌルイ人がこういう言語を使うとごく気軽に静的型情報を捨ててしまいます。「え?型情報?後で必要なときに downcast すればいいじゃないですかぁ。」そして必要もなくゴミ箱を漁るプログラムが誕生するわけだ。そして究極的には

「全部 obj にしてしまえばいろんなクラスに使えますよ!これは実際すごいコード再利用性だ!」

という聞く人が泣き笑うしかない勘違いが実際に出てしまいましたね…ご愁傷様です。

皆さんも知っているでしょう。どんだけ銃は危ないと教え聞かせても馬鹿は銃があれば撃ちたがるものです。 どれだけ注意しても downcast がある限り downcast でやり直しが効くじゃないですかぁと言いながら馬鹿は無意味な upcast を繰り返し、銃を撃ちまくるわけです。巻き込まれる方はたまったものじゃない。こんなことがあるのですから、

「静的型システムは役に立たない」と OO の方がおっしゃり、そして、ダックタイピングにしちゃいなyo!というの私は普通に理解できますね。ノーガード戦法です。(ま、そっから、開発チームが先鋭化少人数化するのは時間の問題ですが…

静的型を使っているのに、それを気軽に忘れてしまうことの言語機能が OO の根幹として必要というところに、矛盾した構造があるわけです。静的型と OO ははじめから上手くいくわけないんじゃないか、ということですね。

OO と静的型の別の世界線

そこで今日ご紹介したいのが OCaml という言語でのオブジェクトの静的型システムです。OCaml のオブジェクトの静的型付けはクラスベースではなく、構造的サブタイピングと呼ばれる、まあ百^百歩譲って「ダックタイピングの静的型版」なのですが、これはあまりここでは重要ではありません。*1 このブログ記事で焦点を当てたいのは次の二つです:

  • Upcast はできるが、より明示的であり、ユーザーに型情報を捨てさせることを意識させる
  • そして、Downcast は………ない!

そうです Downcast は無いのです。できないのです。ゴミ箱漁りはやりたくてもできません。覆水盆に帰らず。そんな馬鹿な、それでは OO できない、と言う人もいるでしょう。しかし、 downcast が無いと言う事は downcast 失敗すると言うこともない。アヒルも鳴かずば撃たれまいに。銃がなければ撃たれることもないのです。凄い割り切りだと思いますね。

そして upcast する場合、普通の OO 言語よりより明示的にその宣言を行わなければいけません。

普通の 静的型付き OO だと:

A a;
B b;
S ss[] = { a, b };

と書くと a と b は配列 ss 中に upcast されて入ることになりますが、 OCaml では同じようなコードは

(* OCaml ではクラスは小文字から始まらなければいけないので A, B, S ではなく ca, cb, cs と書いています。なお変数のスコープとクラス名のスコープは独立しているので new a でも問題ありません *)
let a = new ca
let b = new cb
let ss = [ (a :> cs); (b :> cs) ]

と書かなくてはいけません。

let ss : cs list = [ a; b ]

という書き方はできません。このように冗長に書かなければいけないのは、本当は別の理由があってのことで、あえてユーザーに upcast を意識させるためではないのですが、否が応でもこのオブジェクトはここで upcast しているのだな、俺は今まさに静的型情報を捨てているのだな、point of no return なのだな、ということを認識させる効果があります。静的型を忘れることもできる、できるが、覚悟せよ。(こういうのをホントの静的型の心理学と言う。)

そして失われた静的型情報を取り戻す方法はないのです。ですから

let a' = (List.nth ss 0 <: ca)   (* Upcast は :> なのですから Downcast は <: にしてみました *)
let b' = (List.nth ss 1 <: cb)

みたいな事はできません。できないのですから

「全部 < > (obj みたいなものです) にしてしまえばいろんなクラスに使えますよっ!これは実際すごいコード再利用性だ!」

そんな奴は upcast のし過ぎで自滅!(だって downcast できないからコードの書きようが無い!!)だから私たちはクソコードには出会わなくて済む!!!

Downcast 悪! Downcast 禁止! 動的にクラス検査して静的型回復してもいいことないよ。Downcast 必要だということは、設計に何か間違いがあるはずだ。どうしても OO で書けないなら無理に OO にこだわる必要はない!これが OCaml の考え方です。

なんで OCaml 使えばみんな解決ですね!!と言うほど、ほら私子供じゃないんで。普通の OO 言語だとどういえば良いっすかね。Upcast + downcast はゴミ箱に捨てたものをまた拾ってるようなもんだとか、尻から出したものをまた口に入れるようなもんだってキャミバおじさんも言ってたでしょう!んもう!downcast できるだけしないようなコードを書きなさいって言う位かな…

(じゃあ downcast なしでどうかっこよくプログラム書けるのかということにこの記事は触れていないのでこの記事の自己評価低いですね)

でももちろん downcast 禁止とか受け入れられない人もいるでしょう。そんな人にはやはり OO と静的型には無理があるんだよ! 動的にクラス検査して静的型回復してもいいことないよ。そこまでして無理に静的型にこだわる必要はない。ダックタイピング的考え方です。

どちらにするかは人それぞれです。私や OCaml ユーザーは前者です。後者の人も居るでしょう。私は前者を人に押し付けるほど若く血気盛んでもないので。

うん、歯切れ悪いよね。まあ人間力あると歯切れ悪いよね。

OCaml でなんちゃって downcast (代数的データ型を使って)

(なんかこの辺第一稿ではでたらめ書いていたので書き直しました。なんだか同じ間違いをしたことがあるようなデジャヴュ感あります。やだなぁ)

OCaml には downcast がありません。親クラスに upcast したら元のクラスに静的型を戻す方法がありません。では本当に downcast のようなものが必要になったらどうしたらいいのでしょう。

たとえば OCaml で違ったクラスに属するオブジェクトを同じリストに放り込んで、なんとか元の静的型を維持することはできないのか?関数型言語 OCaml の代数的データ型を使ってみましょう:

type t = A of ca | B of cb

let ss' = [ A a; B b ]

let a' = match List.nth ss' 0 with A a -> a | _ -> assert false
let b' = match List.nth ss' 1 with B b -> b | _ -> assert false

このように明示的に A と B どちらか、という型 t を作ってタグ付けしてやることで別のクラスのオブジェクトを一つのリストに放り込むことが可能です。元の型 ca や cb に戻すにはこのタグをチェックしてやればよろしい。これじゃスーパークラスに upcast して実行時に downcast してるのと変わらないんじゃないの?

うーん、確かに ss' の型は A か B かどちらか、という型になり、元の型から劣化してしまいました。しかししかし、これは upcast で cs という型にしてしまったものより情報は失われていません。 cs からの downcast 先としては(OCaml では元々できませんが) cs のサブクラスが全て候補となりますが、代数的データ型を使っていれば ca か cb この二つに絞られているのが判ります。つぶれた型 t から元のオブジェクトの型を取り戻すことを考えるとき、 ca と cb 以外考える必要は無いことが静的に保障されています。

もちろん新しい型を定義せねばならない、各オブジェクトはタグづけされねばならない、と言う点でプログラムは長くなります。さらにタグはずしのコードも書かねばならない。しかし言語構成上、無闇に upcast を行って銃を撃ちまくると言うコードよりは、余程見通しが良い、実際良い、ということになりましょう。もちろん OCaml でも銃を無闇に撃ちまくる、つまり、無意味にでかい代数型データ型を作ってその中にオブジェクトを没入させまくり意味不明なコードが書けないわけではありません、が、それは downcast 乱発のように気軽にはできません。*2

さてこの方法のもう一つの問題は、リスト ss' は ss と違って、オブジェクトのリストではなく、一段代数的データ型が挟まった t list という型であることです。ですからクラス ss, a, b, に共通するメソッド m をこの要素に対して呼び出すことが面倒になります:

let () = List.iter (fun o -> o#m) ss   (* ss はオブジェクトのリストなので簡単だ *)

let () = List.iter (fun o -> o#m) ss'  これは間違い

let () = List.iter (function           (* こう書く必要がある… *)
  | A a -> a#m
  | B b -> b#m) ss'

これは面倒ですね。コンテナに別のクラスの値を入れたい、共通のメソッドは呼び出しやすいように、でも静的型情報は捨てたくない…うーん、そういうことは OCaml では難しい。やはり downcast 的なことをしようとするとどこかで無理が来るということでしょうか。

OCaml でなんちゃって downcast (Downcast 先を覚えておく)


これは http://caml.inria.fr/pub/ml-archives/caml-list/2006/08/aa0a56475494e183aedc4d2431b30646.en.html に説明されています。Downcast したい型毎にメモテーブルを作って、オブジェクトを登録しておき、downcast したくなったら Oo.id で探し出す方法です:

class ['a] memo () =
object 
  val tbl : (< >, 'a) Hashtbl.t = Hashtbl.create 107
  method add : 'a -> unit = fun o -> Hashtbl.add tbl (o :> < >) o
  method downcast : 'b. (< .. > as 'b) -> 'a = fun o -> Hashtbl.find tbl (o :> < >)
end

class cs = object
  method kuwa = prerr_endline "kuwa"
end

class ca = object
  inherit s
  method x = 1
end

class cb = object
  inherit s
  method y = 1
end

let () =
  let ca_memo = new memo () in
  let a = new ca in
  ca_memo#add a; (* downcast できるように登録 *)
  let b = new cb in
  let ss = [ (a :> cs); (b :> cs) ] in
  List.iter (fun s -> s#kuwa) ss; 
  let a' = ca_memo#downcast (List.nth ss 0) in
  Printf.printf "%d\n" a'#x

これは、後で型を downcast して回復したくなったときのために、downcast する型ごとに目もテーブルを作り、その型のオブジェクトを事前登録しておく、という方法です。オブジェクトは upcast してもその equality (Oo.id というもので同じかどうか判別します)は変わりません。ですから upcast したオブジェクトから元のオブジェクトを復元するには、 Oo.id をキーにしてメモテーブルを探すのですね。

もちろんこれはなんちゃってなので、 weak hashtbl 使わないとリークするとか、そもそもテーブルに山ほど突っ込んだら遅くなるわけですが。

これもっと推し進めて OCaml に RTTI を入れて、全てのオブジェクトは RTTI を保持すれば、 RTTI 検査すれば downcast できるじゃん!というのはあるんですが、これコストがかかる上に、銃を撃ちまくる馬鹿を発生させるので私はヤデスネ。

*1:Downcast がないというか現実的にやってられない理由としてクラスベースではないのでクラス階層をたどる方法より Downcast できるかどうかの判断にコストがかかるというのがありますけど。

*2:これは variant (代数的データ型)ではなく polymorphic variant を使うことでより明らかになりますが、省略します

星のキャミバ様 Adventure Calendar 第727夜: 借家の更新

先日、借家(コンド)の更新しました。

二年前というか 2012年01月01日 から二年契約で借りたものです。

シンガポールでは契約単位は数年(二年がほぼ普通)でその期限内に店子は追い出されることはないのですが、契約が切れると更新時には貸しては自由に家賃を再設定できますし、貸したくなければ更新無しも可能なのですね。日本と比べると貸し手の権利がちゃんと保護されています。もちろん店子としては契約期限が迫ってくるといろいろと心配することになります。契約継続に関する相談は契約が切れる二ヶ月前くらいには議論することになっています。

で、一年前くらいだと家更新時に家賃が上がった!という twitter の呟きが多かったので、私たちも心配していました。そうでなくともそもそも大家側が契約更新を望まない場合もなきにしもあらず(大家はマレーシア人でマレーシア在住なのですが、子供や親戚がシンガポールに住むことになったわーだからどいて、とか普通にある)ですから、年末年始の日本への一時帰国はせず、10月に帰ってきました。私たちはまあ家賃は今高いんだけど(特に二年前から円安に突入して心情的にはかなり高くなってしまいました)MRTないけどバス路線は思ったよりよいし、シンガポールにもかかわらず海風のおかげで基本的に冷房いらないし(昼は暑いけどね…夜はいらない)、なにより周りの人は良いので動きたくなかったのです。(昼間は日本人駐妻の相互監視で空気が波打つという高層コンドとか無理なので)

したら家賃上げなしで更新になりました。一安心です。まあ九月ごろからいろいろ家に問題があって、特に上の人のキッチンから水漏れが発生しているのに、上の人居なくなった(引越しした?)というトラブルがあり、大家責任ではないにしても印象下がっているから上がらなかったのかな?と納得しています。(水漏れは自然と止まりましたが泊まりがけで遊びに行くときは相当心配しました。)

で、不動産エージェントやっている知り合いにも聞きましたがちょっと前と変わって今は家賃上げられない状況だそうですね。私はややこしいことわかりませんが、助かりました。まあ良くも悪くも資本主義原則で動いているのでこういうのは水物ですねー。

でまあ、私がクビにならない限り後二年ここに住みますので、遊びに来たい人はゆっくり遊びに来てね!

家賃更新儀式の時に大家側のエージェント Randy 君がエレベータに閉じ込められたのは印象的でしたね。帰りがけにしっかり、もしシンガポール長く居るなら購入とか、どう?とかしっかり営業していきました。さすが。んーでも購入まではまだ踏ん切りつかないなー。今銀行勤めですからスタッフの金利すごくいいらしんですけどね!

Launch CamlP4 with some help of OCamlFind

I do not know why but there is no ocamlfind camlp4o. Here is a tip for this:

#!/bin/sh

# \ mark is actually a backslash.

camlp4o \
    `ocamlfind query -r -i-format $*` \
    `ocamlfind query -r -a-format -predicates syntax,preprocessor $*`

I named this script as ocamlfind-camlp4o. With this, you can launch P4 with your packages like ocamlfind-camlp4o pa_ovisitor orakuda.syntax.

すぐできるキモイ OCaml 改造メモ

糖衣構文で可能 (P4 マター)

  • 関数にする (.[1]) (.[]) (.(1)) (.()) (.field) (#meth) (,,,)
  • どんどん関数にする (.[]<-) (.field<-) などなど
  • もっともっと関数にする {.. with x = e} など (.. が無いとさすがに怖い)

糖衣構文ではできない (コンパイラ改造コース)

  • open M の名前の一部を隠蔽
  • SML 方式のオーバーローディング
  • SML の #2 のようなもの (OCaml では #2 は行番号指定記号なのでこの書式は使えない)

OCaml 4.01.0 変更点

スペルチェック


識別子のミスタイプがあった場合訂正を提案します:

# List.lentgh
  ;;
Characters 0-11:
  List.lentgh
  ^^^^^^^^^^^
Error: Unbound value List.lentgh
Did you mean length?


個人的には、はあだから?と言った感じかと思っていたのですが、今のところスペルミスを指摘されるとそれはそれでうれしいことが判明しております。

Parser rewriter -ppx


CamlP4 がヘビー過ぎて使えないという虚弱体質な人のために新しい -ppx という物が入りました。はっきり言うと P4 をマスターしている 人には機能が縮小されたものなので嬉しくありません。 まあ使い慣れた Parsetree のレベルでいじれるのが幸いですが…

ocamlc -dsource


コンパイラが受け取ったパースツリーを OCaml の形に出力する機能が付きました。 P4 が組み合わさった複雑なコードがどう展開されているのか確かめるのに役に 立つような気がしますが、ならば P4 を動かしたほうがコメントも残るので 良いのではという気がしますが。(ああ、 -ppx の結果を確認するのに使うつもりなのかな?)

-short-paths というオプションを使うと、データ型に複数の名前がある場合、短いものを表示


これは便利。モジュールやファンクタを沢山使用したコードでは、例えば、:

(* x.ml *)
module Very = struct
  module Long = struct
    module Module = struct
      module Name  = Hashtbl
    end
  end
end

open Very.Long.Module
let tbl = Name.create 107
let () = Name.add tbl 1 2


のようなものを ocamlc -i x.ml コンパイルすると:

val tbl : (int, int) Very.Long.Module.Name.t


と表示されていましたが、 ocamlc -short-paths -i x.ml とすると

val tbl : (int, int) Hashtbl.t


となります。 -i だけでなく型のプリンタで全て有効。 Core や Batteries を 使っていて標準の型の長い別名があっても短い方に寄せてくれるので型エラーが 読みやすくなります。まあおせっかいに感じることもあるかもしれませんね。

open が既存の名前を覆い隠す時に警告


x が定義済みのところで M.x が存在するモジュールを open M とすると 既存の x の名前が密やかに横取りされるのですが、それに対する警告です:

module M = struct let x = 666 end

let x = 42
open M

let () = Printf.printf "The answer is %d\n" x


これを ocamlc -w A コンパイルすると:

Warning 44: this open statement shadows the value identifier x (which is later used)


という警告が出ます。モジュール M の事を知らない人が不用意に open してしまうと x が違う物になってしまうから警告を出すのですね。いや、俺は判ってやってるから文句言わないでくれ、という場合は open M の代わりに open! M と書くと警告がでなくなります。


しかしこれ警告が出たとして直す方法があまりないのですね…コード内の変数の名前を変える、ここでは let x = 42 let y = 42 にしてその後の x を全部変名する位しか無い。 open M hiding (x) の様な open の全開の挙動を制限する仕組みが無いと警告されても結局無視される恐れがあります。


なお、モジュール名についてはこのチェックが働いていないので中途半端:

module M = struct
  module List = struct
    let length = 1
  end
end

open M

let x = List.length


List が上書きされるの気持ち悪い人いるはずですね。

アクセント付きアルファベットは識別子名に使えなくなった


OCaml はフランス製なので Latin-1 のアクセント付きアルファベットが 長らく使えたのですが、4.01.0 から deprecated として警告が出ます:

# let caract〓re = 'c';;
Characters 4-11:
  let caract〓re = 'c';;
      ^^^^^^^
Warning 3: deprecated feature: ISO-Latin1 characters in identifiers
Characters 11-12:
  let caract〓re = 'c';;
             ^
Error: Illegal character (\168)


[A-Za-z0-9_'] しか識別子に使えません。国際化に背を向ける漢らしい変更ですね!

OCAMLPARAM 環境変数コンパイラのスイッチのデフォルト変更/オーバーライド可能


オプション -g を付けてデバッグシンボルを付けたい、-annot や -bin-annot を付けて 型情報や定義に飛びたいと思っても第三者のソフトウェアでは Make スクリプト にその指定がない場合、そのスクリプトを一々変更する必要があり大変でした。


OCAMLPARAM 環境変数を使うと一気にコンパイラのスイッチのデフォルトの値や コマンドラインから指定された後、さらにスイッチの値をオーバーライドすることが できるようになりました。便利です:

export OCAMLPARAM=_,g=1,bin-annot=1


とする(bash)と ocamlc を使った場合、 -g -bin-annot が常に付いた 状態になります。 _ の左辺はデフォルト値の変更、右辺はオーバーライドの 変更です:

export OCAMLPARAM="cc=xcc,_,runtime-variant=foo"


だと C コンパイラとしてはデフォルトで xcc を使います。ただし ビルドスクリプト内部で -cc gcc と明示された場合は gcc が使われます。 一方ランタイムは常に foo というものを使うことになります。 これは -runtime-variant bar スクリプトで指定されていたとしても有効です。

Printexc.get_callstack


コールスタックが欲しい深さで取れるようになりました。

演算子 |> @@


F# の |> Haskell $ がそれぞれ |> @@ という名前で入りました。めでたい。 私は Haskell $ として & を使い続けますが。

型コンテクストから明らかなコンストラクタとレコードラベルのモジュール名が省略可能


文面から明らかな場合はモジュール名書かなくてよくなりました:

module M = struct
    type v = Foo | Bar
    type r = { x : int ; y : int }
end

let x : M.v = Foo  (* M.Foo と書かなくても良い *)
let f = function M.Foo -> 1 | Bar -> 2 (* M.Bar と書かなくても良い *)
let n = f Bar  (* M.Bar と書かなくても良い *)

let g r = r.M.x + r.y (* r.M.y と書かなくても良い *)


型が不明の場合はエラーになります。型が明らかな場合でも念のために警告40を 出してきますので使いたい場合は無視対象とすること。


SML の演算子オーバーローディングのコンストラクタやレコードフィールド版かと思いましたが該当部分のコードを見たところコンストラクタの型推論時に型がわかっている場合のみ動くみたいです。つまり後から型コンテキストがわかる場合は救われない。 [M.Foo; Foo] は良いが [Foo; M.Foo] では駄目。不明なコンストラクタやフィールドの推論を delay するのは可能だが面倒なので、まあそんなものかねえ。

caml_modify() と caml_initialize()


PR#6019: more efficient implementation of caml_modify() and caml_initialize(). The new implementations are less lenient than the old ones: now, the destination pointer of caml_modify() must point within the minor or major heaps, and the destination pointer of caml_initialize() must point within the major heap.


ああうう。これは覚えておかねば既存 C コードでクラッシュするかもしれない…

オブジェクトファイルのリンク順

OCaml のオブジェクトファイル(.cmo, .cmx)を並べる順番には意味がある。 順番を間違えると:

Reference to undefined global Hogehoge


などと言われるので注意。


a.ml:

let x = 1


b.ml:

let y = A.x


c.ml:

let z = B.y


というソースがあったとする。 a.ml, b.ml, c.ml の順に分割コンパイルする。これは問題ない:

$ ocamlc -c a.ml
$ ocamlc -c b.ml
$ ocamlc -c c.ml


さて、これをリンクする場合、依存関係の順にリンクしなければいけない:

$ ocamlc -o a.out a.cmo b.cmo c.cmo    # a.out 実行ファイルへとリンク


これを間違えると Reference to undefined global Hogehoge というエラーが出る:

$ ocamlc -o a.out b.cmo a.cmo c.cmo
File "_none_", line 1:
Error: Error while linking b.cmo:
Reference to undefined global `A'


OCaml でのモジュール毎の分割コンパイルと、そのリンクは、モジュール群のソースが、 連結されて一つの巨大な OCaml プログラムソースになったものをコンパイルする 作業を分割したもの、と考えると判りやすい。 b.cmo, a.cmo, c.cmo の順番での リンクは、 b.ml, a.ml, c.ml をこの順番でつなぎあわせたものをコンパイルするのと 同じで、b.ml の部分では a.ml のモジュール A は未定義。だからエラーになる:

module B = struct

    let y = A.x

end

module A = struct

    let x = 1

end

module C = struct

    let z = B.y

end


この上記のプログラムがコンパイルエラーになるのと同じである。


これは cma ライブラリを作る際の落とし穴にもなる ocamlc -o lib.cma b.cmo a.cmo とした場合、 lib.cma はエラーもなく作成される。その後、この lib.cma を使って例えば c.cmo とリンクし、実行ファイルを作ろうとすると、そこで初めてエラーとしてレポートされる:

$ ocamlc -a -o lib.cma b.cmo a.cmo       # lib.cma アーカイブ作成。エラー無し
$ ocamlc -o a.out lib.cma c.cmo          # a.out 実行ファイルへとリンク(失敗する)
File "_none_", line 1:
Error: Error while linking lib.cma(B):
Reference to undefined global `A'


上記の lib.cma は A というモジュールに依存した B モジュールと、それと独立した A モジュールを含むアーカイブになっている。大変に気持ち悪いがこのようなことができる:

$ ocamlc -o a.out a.cmo lib.cma c.cmo


この実行ファイルには A というモジュールが二回リンクされている。lib.cma 内部の B が使う A は lib.cma 内の A ではなく、 lib.cma の前に並べた a.cmo になる。同名モジュールが二回出てくることは OCaml のソース上ではあまりにわけが分からないので禁止されているが、リンカ上では OCaml の普通の値が同名の変数に束縛された場合 shadowing されるように先出のモジュールは後出のものに shadowing される。気持ち悪いがそういう挙動である。


このような問題を避けるにはモジュールを依存順に並べてリンクすればよいのだが、Makefile などの場合は手でモジュールリストの順番を調整する必要がある。 OMake などではこの依存関係を自動解析してくれるのでほとんど気にする必要はない…ただしモジュール間に渡る副作用の依存関係が存在していない限りにおいて、である。

List の ; と tuple の ,

List, Array, Record などの要素区切りの ; はとても奇妙に見えるかもしれない。 OCaml を使っている人でもあまり意識していないのだが、実は一貫性がある。

  • A ; B は B ; A と書いても型は変わらない
  • A , B は B , A と書くと型が変わる場合がある


例:

print_string "hello"; print_string "world"
{ a = 42; b = "hello" }
[ 1; 2; 3 ]
[| 1; 2; 3 |]


これらの要素を入れ替えても型は変わらない。もちろん意味は変わる場合がある。


例:

(1, "hello")
(int, float) Hashtbl.t


これらの要素を入れ替えると型が変わってしまう。