オブジェクト指向を行使する心 ではオブジェクト指向の必要性と仕組みについて議論した。
インスタンスは言語によって様々な実装方法があるが、大きく分けて「クラス(処理)のインデックス」か「処理そのもの」のどちらかがインスタンスの内部に隠れている。
と述べたが、Haskellの場合、クラスのインデックスに基づいた表現では、インターフェイスは型クラス、クラスはインスタンス、インスタンスは存在量化された型の値に対応する。…といってもややこしいことこの上ないので、実装例を考えてみよう。
まず、問題となっている愚直な実装は、Haskellではこんな感じだ。
data World = World { … } data SceneId = Menu | Map | Battle draw :: SceneId -> World -> IO World draw Menu = … draw Map = … draw Battle = …
Worldは描画に必要なすべての情報が入っている、グローバル変数のようなものと考えてよい。新しいシーンを追加するにはSceneId
とdraw
を両方書き換える必要があるため扱いにくく、シーンではなくキャラクターなどを管理するとなればなおさらだ。
存在量化を用いると以下のように書ける。
data Menu = Menu data Map = Map data Battle = Battle class Scene a where draw :: a -> World -> IO World instance Scene Menu where draw = … instance Scene Map where draw = … instance Scene Battle where draw = … instance Scene SomeScene where draw (SomeScene a) = draw a data SomeScene = forall a. Scene a => SomeScene a
data Menu = Menu
のくだりは若干ばかげているようにも見えるが、定義にシーン固有の値を追加してもよい。各シーンを表す値であるMenu
、Map
、Battle
をSomeScene
で包むと、内部にはインスタンスを識別するためのタグが入り、型を共通にしつつ実行時に処理を切り替えられる。標準ライブラリのControl.Exception
ではこの方法を採用しており、便利ではあるがSomeScene
のようなものを毎回作らなければならないのがいまいちだ。
インスタンスに直接処理を格納するアプローチとしてはHaskell's overlooked object system*1のHListがある。異なった型の要素を持てるリストを用い、操作の直接的な集まりとしてインスタンスを表す。
import Data.HList draw = Label :: Label "draw" type Scene = Record '[Tagged "draw" (World -> IO World)] sceneMenu :: Scene sceneMenu = draw .=. … .*. emptyRecord sceneMap :: Scene sceneMap = draw .=. … .*. emptyRecord sceneBattle :: Scene sceneBattle = draw .=. … .*. emptyRecord
こちらはシーンを値として定義できるのが魅力だ。演算子(#) :: HasField l r v => r -> Label l -> v
でdraw
の処理を呼び出せる。しかしその裏の仕組みがとても複雑なうえ、IORef
などを用いないと状態をまともに扱えないのが難点である。
objectiveはそのどちらでもなく、オブジェクトの中身は「操作を解釈する関数」になっている。
newtype Object f g = Object { runObject :: forall x. f x -> g (x, Object f g) }
あくまで操作とオブジェクトは別の概念として扱うのが特徴で、種が* -> *
ならば任意のデータ型を操作として利用できる。Scene
型のオブジェクトはSceneOp
を受け取りStateT World IO
の型のアクションを生み出す。
import Control.Object data SceneOp a where Draw :: SceneOp () type Scene = Object SceneOp (StateT World IO) sceneMenu :: Scene sceneMenu = Object $ \Draw -> … sceneMap :: Scene sceneMap = Object $ \Draw -> … sceneBattle :: Scene sceneBattle = Object $ \Draw -> …
runObject
にオブジェクトとDraw
を渡すと、オブジェクトはDraw
の結果と次のオブジェクトを返す。これをそのまま使ってもよいが、new :: Object f g -> IO (Instance f g)
を用いてインスタンスを生成することもできる。(.-) :: Instance f g -> f x -> g x
を使うと、インスタンスは次のオブジェクトで置き換わるため、広く使われているメソッド呼び出しも可能だ。
オブジェクトは関数と同様のコンポーザビリティを持ち、既存のオブジェクト指向の実装を超える拡張方法も提供する。具体的な利用については以下のちゅーんさんの記事で詳しく述べられているので、こちらも参照されたい。
存在量化、HList、objectiveは多態を実現するが、それぞれ表現方法は全く異なる。純粋、ファーストクラス、合成可能、この言葉にピンときたらobjectiveを是非使ってみてほしい。
*1:Oleg Kiselyov, Ralf Lämmel , http://arxiv.org/pdf/cs/0509027.pdf, 2008