Hatena::ブログ(Diary)

Oh, you `re no (fun _ → more) このページをアンテナに追加 RSSフィード

Ah well, a dromedary has one hump and a caml has reference cells, buffers, and a garbage collector.

2014-06-11

大事なことは全部MLが教えてくれた 〜 Apple の Swift の mutability 周りの件を理解する

開発者アカウントに金が出せない貧乏人の方々が、次の AppleSwift のコードの挙動がわからない、というので盛り上がっております:

let a = [1,2]          // a = [1,2]
var b = a;             // b = [1,2]
b[1] = 3;              // a = [1,3]  b = [1,3]
b.append(5);           // a = [1,3]  b = [1,3,5]
b[1] = 4;              // a = [1,3]  b = [1,4,5]

もちろんわたしも貧乏ですからわかりやすい炎上案件を待っておるわけです。これはわかりやすいわからないが来たね。

だいたい

  • b[1] = 3 とやると a[1] も変化する、これがわからないという人
  • b[1] = 4 とやると a[1] が変化しない、これがわからないという人

二種類いるようです。私はまず、 b[1] に代入できることがわかりませんでしたのでそれ以前の人間です。

こういうとき、このコードが何をやっているかを理解するには同じ挙動をする ML のコードを書くとわかりやすいです。なぜ ML かというと、もちろんみんなの好きな関数型言語であって、かつ代入などの副作用を持っていて、でも基本は不変データなので可変データの部分はちゃんと明示してやらなきゃいけない言語なので、こういう副作用でわけわからんコードを読み解くには明示する分都合がよろしいからです。もちろんみんなが好きな関数型言語というのはここではとくに意味がありません。Haskell は本題に行く前に Haskell は純粋で純粋なのに副作用があったりなかったりうだうだモナドがああだこうだで圏論があしたどしたでドヤってめんどくさ過ぎるのでこういうのには向きません。

ええっと、断っておきますが、これは上の Apple の Swift コードの挙動と同じ挙動を持つ一番単純な ML のコードって何かしらん、という試みです。オッカムの剃刀の原理を信じれば、そのコードと同じことがおそらく Apple の Swift の中で起こっておるであろう、ということです。実際の Apple の Swift の中がどうなっているかは私貧乏ですから知りません。ただ、まあ ML のコードを見れば、まあ多かれ少なかれ、こんな内部実装なんであろうなあ、と十分推測できるわけです。

そんな手探りせずに仕様書読めという突っ込みもありですがね。まあこういう群貧撫AppleのSwiftみたいな話もたまにはいいんじゃないですか、仕様書探すの難しいらしいし (http://cpplover.blogspot.jp/2014/06/appleswift.html)、どんなプログラミング言語であれ、仕様書を読まないとわからない言語仕様というのはできるだけ少ないに越したことはありません。

で、書くとこんなんになります。 http://try.ocamlpro.com/ で一行づつ入れながら、途中で a;; とか b;; して変化を見ながら試してみるといいよ:

let a = [| 1; 2 |];;
let b = ref a;;                (* 明示的に参照ですって言わないといけないんです *)
!b.(1) <- 3;;                  (* 参照の中身を見るには !b って書かないといけないんです *)
b := Array.append !b [|5|];;   (* 参照先を変えたいときは b := ほげほげ って書くんです *)
!b.(1) <- 4;;                  (* 配列のn番目要素は a.(n) て書くんですキモイ *)
  • b[1] = 3 とやると a[1] が変化するのがわからない人は、 var b = a がコピーは行わないってことをわからない人です
  • b[1] = 4 とやると a[1] が変化しないのがわからない人は、b.append(5) が b 自体を変えてしまうことがわからない人です、ってかこれ普通わかんないでしょ

でもこれ両方とも ML で書いたコードではハッキリわかりますよね。少なくともこの例を見る限りは、

  • var b = e は e の値への参照を作るだけでコピーはしない
  • b.append(5) は参照する値に 5 をくっつける。配列だから結果はコピーになる。 b はそこに参照先を変える

という意味だとわかる。(Array.append はコピーするんですよ。というか配列の後ろに何かつけてもコピーじゃないってそんなすごい配列があったらそれは配列じゃない) copy-on-write とかカッコイイ機能を入れようとしたけどバグっているんだ!とかそういうオシャレ事案ではない。

クソですね。

オブジェクトメソッド呼び出しのような外見なんだけど、呼ばれた後、オブジェクト自身は他のに摩り替わっている。高度なナリスマシ事案だ。これはヒドイ仕様だと思います。 ML なら b := Array.append !b [|5|] と書くわけなので、 b の参照先が変わっていることがわかる。append すると配列がコピーされるかどうかはもちろん配列というデータ構造の特性を知らないといけないのであれだけど。

せっかくだから Apple の Swift の人の立場になって考えましょう。これはどう直せばいいですかね。

  • var b = e は e の値のコピーを作ってそれを参照することにする

これはパフォーマンス落ちる…のなら、気休めに copy-on-write 導入しますか。 b[1] = 3 とやっても a[1] が変わらない… まあよく判んない人相手にはこの挙動が一番いいのかもしれませんね。

  • 配列を頭よくして b.append(5) とやっても b の中身は成りすましにならず、ちゃんと b[1] = 4 の後 a[1] = 4 になる。

これは配列はもはや配列ではなくて何か別の rope みたいなものになります。うーんそれは配列ではない。

  • b.append(5) などというクソいメソッドはやめて b = b ++ [5] みたいにする。

これならコピーしていることがわかる?かな?いやー無理でしょうねぇ。でも少なくとも現時点での挙動でメソッド呼び出しの形は誤解をまねきやすすぎるよね。

この中でどれが一番いいか…やはり ML を使うことだと思いますね。おわり。

2013-12-09

金融と OCaml

関数型言語というと金融で流行っている、特に OCaml と言えば金融御用達関数型言語として語られることも多い。金融業界に関数型言語を持ち込み成功した最初の二社 LexiFi とJane Street Capital は共に OCaml を採用し、そして二社とも今日成功しているからだ。

今回はそのうち LexiFi について話したい。私が Ph.D を取った後、一年間、産業ポスドクとしてお世話になった会社である。

金融派生商品(デリヴァティブ)は様々な日時や条件、依存する別の商品価格によって価値が変化する。デリヴァティブは先物やオプションなどのような一般市場でも流通している簡単な派生商品だけでなく、会社間でだけで取引される非常に複雑な物もある。これらの商品にはそれぞれ業界内で通用する名前が付けられ取引、評価、管理が行われてきた。そしてこれらを扱うプログラムもまた、それぞれの商品ごとに別個の機能として実装されてきた。結果としてプログラムやデータ種は膨大なものとなり、その開発やメンテナンスは非常に煩雑なものとなった。例えば金融機関間でこれらの商品のデータをやり取りする規格であるFpML(http://www.fpml.org)はこの実情に合わせた非常に大きなものになっている。

(もちろん FpML に意味がないわけではない…会社間での商品情報の交換には良いだろう。だがモデリングとしては完全に誤ったアプローチである。新しい商品を作ったとして、それが現行の FpML で記述できなければ、FpML 改訂を働きかけなければならないわけだ。そして改定されれば FpML は更に巨大になる。)

これらの金融派生商品一つ一つを見ると様々であり、その仕様も複雑なものもあるが、実はその全ては簡単な人工言語で記述できる。そのことに気付いた LexiFi(http://www.lexifi.com)の Jean Mark Eber は世の中に出回っている金融派生商品のほぼ全てが時期選択や条件分岐などの単純な15個ほどのプリミティブの組み合わせで記述できることを示した。

複雑な数多くの商品を数少ないプリミティブに還元できる、これは大変に強力である。各プリミティブはとても簡単なので、それらの意味や、評価方法は簡単に書くことができ、複雑な商品はこれらの組み合わせから自然に演繹できる。もはやそれぞれの商品ごとに面倒な実装をし直す必要はなくなったのだ。

LexiFi はこの商品記述言語システムを関数型言語 OCaml で実装した。これはまったく自然な選択だった。 http://d.hatena.ne.jp/camlspotter/20131105/1383619506 (そうです。これはこの記事を書くための前座だったのです)でも書いたように、言語を代数的に扱うならばオブジェクト指向言語よりも代数的データ型を持つ関数型言語が適しているし、構成されるプリミティブに第一級関数が使われていたからでもある。

もう少し詳しく話すと、商品記述言語自体は、 OCaml言語ではない。もっと単純なものだ。これは OCaml の代数的データ型として表される。これを組み合わせたり、分解するための操作言語として OCaml を使う。つまり、今流行りの EDSL である。そのころ私達はそんな単語も知らなかったのだが… この EDSL を使って商品記述を解析し、適正な値段をモンテカルロ法シミュレートしたり、次回支払いの期日を管理したり…様々なアプリケーションがこの OCaml 上の DSL を使ってどんどん書くことができる。

事実、この言語化によるパワー、金融商品の代数的表現を使って LexiFi はデリバティブのモデル化、評価、マネージメントなどの高度なシステムを少人数、短期間で開発し、現在に至っている。

この関数型言語による金融派生商品のモデル化は LexiFi だけでなく現在では複数の大手の金融機関でも採用されている。その一つが今私が働いている銀行である。この仕事場も元を辿れば LexiFi のメンバーからの紹介によるもので私は LexiFi には感謝しきれない。

数多くの対象を一つ一つ個別に扱うのではなく、それらを記述できる単純な言葉を見つけ出し、その言葉の構成子に対して分析やプログラミングを行う…無論これは多くのプログラマー、特に代数的データ型に慣れた関数型言語プログラマーには当然の事なのだが、金融界ではそれ以前にはぜんぜん知られていなかったんだよ、と彼は私に語っている。

問題を上手く言語化できれば問題の九割は解けたようなものだ、と私は考えるようになった。ただしそのためには問題への深い理解が不可欠なのだが…

2013-12-03

OCaml がおかしくなったとき

違うバージョンのコンパイラを違うバージョンのライブラリセットに対し間違って使ってしまった

  • コンパイラが変われば元のコンパイラで作成したオブジェクトやライブラリは新コンパイラではまず使えない clean せよ
  • which ocaml / which ocamlc / which ocamlopt
  • ocamlc -where /ocamlopt -where でライブラリディレクトリを確認
  • env | grep -i caml で各種ディレクトリの指示先が使っているコンパイラと齟齬が無いか grep -i ocaml ではなく caml であることに注意
  • $PWD/.ocamlinit $HOME/.ocamlinit が何か悪さをしていないか
  • strace でどのファイルをなめているか見る
  • 他人の作ったバイナリディストリビューションは信用しない

2013-10-28

静的型と 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 を使うことでより明らかになりますが、省略します

2013-08-24

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

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 などではこの依存関係を自動解析してくれるのでほとんど気にする必要はない…ただしモジュール間に渡る副作用の依存関係が存在していない限りにおいて、である。