OCamlのclassを使ってみた

来月の入社前の準備として、今迄あまり触れてこなかった技術を勉強しています。まぁ主にJavaScriptとかその周辺なんですが。やっぱりJavaScriptは変態言語だと思いました。OCamlのこと言えません。

そんなOCamlの中で個人的に一番よくわからないものとして、OCamlのO、オブジェクト指向の機能があります。個人的な利用で使う機会があり、色々と試してみたりしましたので、個人的なメモ代わりに残しておきます。

OCamlのクラスの基本

OCamlのクラスは、object〜endの間に定義を書くことで作れますが、この時点でまず大抵のオブジェクト指向の言語でいうクラスと大分離れます。何でかというと、↓のようなことが成り立つからです。

# class t = object
    method b = 10
  end;;
# let a = object method b = 100 end;;
# let c = new t;;
# let test : #a -> int = fun x -> x#a;;
# test a;;
100
# test c;;
10

普通で言えば、class 〜 で定義をして、newでインスタンスを作る、という感じですが、↑のlet a は、 object〜endをそのまま書いてしまっています。私の理解では、これはRubyとかPythonのダックタイピングそのものです。というか、ダックタイピングしか無いと言った方が正確なんでしょうか。

一応オブジェクト指向を謳っているので、当然ながら継承もできます。しかし、これも継承しようがしまいが、インターフェースさえ合ってしまえば、同じ型に突っ込むことができます(ちょっと語弊あり)

# class t1 = object
    method a = 100
  end;;
# class t2 = object
    method b = 10.0
  end;;
# class t3 = object
    inherit t1
    inherit t2
  end;;
# let a = new t3;;
# let b = object
  method a = 5
  method b = 1.0
  end;;
# let test x = Printf.printf "%d : %f" x#a x#b;;
# test a;;
100 : 10.0
# test b;;
5 : 1.0

こんな具合です。また、場合によっては継承しても、継承先のクラスのインターフェースが、継承元のインターフェースを含んでいる保証もありません。シグネチャが定義されていれば別かもしれませんが、そうでない場合には、継承はis-a関係を表すことは全く保証されません。
まぁ、Javaとかでもis-aを完全に満たすように継承関係を設計するのはかなり難しかったような気がするのでいいような気がします。事実あまり気になりません。

OCamlのクラスを使うとき

OCamlのクラスには基本的な使い方以外に、コアーションと呼ばれるアップキャスト?や、多相クラス、多相メソッドなど色々と楽しそうな機能がありますが、私の利用範囲ではほとんど利用しませんでしたので・・・。

さて、オブジェクト指向の言語、例えばJavaでは、クラスにも種類があって、抽象クラスとインターフェースとかがあります。最近ではインターフェースを活用するのが主なようですね。この間ちょうどよくScalaを勉強しましたが、OCamlのクラスはどちらかというとScalaのtraitではないかと思います。実装も持てるしインターフェースとしても使える、いくつでも継承(ミックスイン)できる、という点で。

しかし、OCamlのクラスは型の付けかたが致命的なほどに違うため、あまり他の言語の知識は役立たないような気もします。こればっかりはどうしようもないですね。
OCamlのクラスは、前述したように「継承してもインターフェースが一致するとは限らない」という問題があります。ということは、実装を継承して、継承先でオーバーライドなんかしちゃったら、その時点でもう型が変わってしまう可能性があります。これは、自分自身を引数にとるようなメソッドを継承したときに顕著になるそうです。
例えば、オーバーライドしたメソッド内で、継承先の内部メソッドを利用しており、かつそのメソッドが自身を受け取るようにした場合、これはバイナリメソッドと呼ばれるメソッドのようですが、これはOCamlのクラスシステムでは、型が多相的ではなくなってしまうため、まったく同一のメソッドを持つならともかく、バイナリメソッド内で、継承元のクラスに無いメソッドなどを利用した場合には、もはやサブタイプではなくなるため、渡した際に型エラーが出ます。

というわけで、OCamlでバイナリメソッドを使いたい場合、クラス外に用意する必要があります。これなら部分型を利用できるため、なんとでもなります。OCamlのクラスは、関数の型として多相クラスが渡せるようになっていれば、実装はどうのこうの言われません。
つまり、OCamlでクラスを積極的に使っていくときは、

  • バイナリメソッドは使わない
    • 使いたくなったら同じモジュール内とかに関数を定義してやればいい
  • クラス実装の継承はしない
    • 実装を継承してしまうと、内部で利用しているメソッドまで見えてしまう
  • 実装がいらないようなら、class typeだけを用意しておいて、inheritで継承する
    • class typeしかないクラスは、class typeにしかinheritできない→実装は好きにできる

ような感じでクラス構造を設計してやればなんとかなりそうな感じがしました。簡単な例としては↓みたいな感じでしょうか。

# class type t = object
    method a : int
  end;;
# class type u = object
    method test : int -> float
  end;;
# class inh :
  object
    inherit t
    inherit u
  end = object
    method a = 100
    method test i = 1.0
  end;;
# class inh2 : object inherit t
 end = object (self)
    method private b = 1.0
    method a = int_of_float self#b
  end;;
# let temp (x:#t) = x#a;;
# temp (new inh);;
- : int = 100
# temp (new inh2);;
- : int = 1

class typeはシグネチャであるため、シグネチャにinheritしか無い場合、内部でだけ利用するようなメソッドは、class typeに同様に宣言を書くか、内部メソッドとしてprivateを付けて定義すればなんとかなります。

どこかで見ましたが、OCamlのクラスは「多相的レコードを使いたくなったらクラスを使えばいい」みたいな感じらしいです。機能を漸進的に付け加えるときとかにも、というのを見たような気がします。つまりは、レコードの代わりくらいにつかう、という気分でちょうどいいらしいです。

おわりに

自分自身の理解が追い付いていないので、なんとも論旨がまとまらない文ですが、結論としては「OCamlでクラスは積極的に使う理由は無い」という感覚です。制限が多いというよりかは、積極的に使ってしまうと、逆にオブジェクト指向本来の複雑性と、OCaml独特のクラスシステムが相俟ってもう手が付けられない、というような。
SDL + OpenGLでサンプルを作ってみているときに、クラスを使ってみようとしたらかなりドツボにはまってしまったというのもあります。後、部分型を適切に設計するのが非常に難しいですが、これはオブジェクト指向の問題そのものでもあるので私の能力不足ですね。

まぁ、OCamlでクラスを積極的に使おうとするよりかは、レコードとモジュールでなんとかならんか考えてみる方がよっぽど正しそうな気がします。今なら一級モジュールとか使えますし。使ったことないけど。