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