読者です 読者をやめる 読者になる 読者になる

Haskellでいかに多態を表すか

オブジェクト指向を行使する心 ではオブジェクト指向の必要性と仕組みについて議論した。

インスタンスは言語によって様々な実装方法があるが、大きく分けて「クラス(処理)のインデックス」か「処理そのもの」のどちらかがインスタンスの内部に隠れている。

と述べたが、Haskellの場合、クラスのインデックスに基づいた表現では、インターフェイスは型クラス、クラスはインスタンスインスタンス存在量化された型の値に対応する。…といってもややこしいことこの上ないので、実装例を考えてみよう。

まず、問題となっている愚直な実装は、Haskellではこんな感じだ。

data World = World { … }
data SceneId = Menu | Map | Battle

draw :: SceneId -> World -> IO World
draw Menu = …
draw Map = …
draw Battle =

Worldは描画に必要なすべての情報が入っている、グローバル変数のようなものと考えてよい。新しいシーンを追加するにはSceneIddrawを両方書き換える必要があるため扱いにくく、シーンではなくキャラクターなどを管理するとなればなおさらだ。

存在量化を用いると以下のように書ける。

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のくだりは若干ばかげているようにも見えるが、定義にシーン固有の値を追加してもよい。各シーンを表す値であるMenuMapBattleSomeSceneで包むと、内部にはインスタンスを識別するためのタグが入り、型を共通にしつつ実行時に処理を切り替えられる。標準ライブラリのControl.Exceptionではこの方法を採用しており、便利ではあるがSomeSceneのようなものを毎回作らなければならないのがいまいちだ。

インスタンスに直接処理を格納するアプローチとしてはHaskell's overlooked object system*1HListがある。異なった型の要素を持てるリストを用い、操作の直接的な集まりとしてインスタンスを表す。

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 -> vdrawの処理を呼び出せる。しかしその裏の仕組みがとても複雑なうえ、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を使うと、インスタンスは次のオブジェクトで置き換わるため、広く使われているメソッド呼び出しも可能だ。

オブジェクトは関数と同様のコンポーザビリティを持ち、既存のオブジェクト指向の実装を超える拡張方法も提供する。具体的な利用については以下のちゅーんさんの記事で詳しく述べられているので、こちらも参照されたい。

tune.hateblo.jp

tune.hateblo.jp

存在量化、HList、objectiveは多態を実現するが、それぞれ表現方法は全く異なる。純粋、ファーストクラス、合成可能、この言葉にピンときたらobjectiveを是非使ってみてほしい。

*1:Oleg Kiselyov, Ralf Lämmel , http://arxiv.org/pdf/cs/0509027.pdf, 2008