Haskellでもできる!実践・オブジェクト指向
Lensにほとんど触れたことのない人にはこちらの記事がオススメです:Lensで行こう! - Just $ A sandbox
Haskellでもオブジェクト指向をしましょう!
Haskellは直接オブジェクト指向的な機能を提供してはいませんが、我らがLensの力を借りることでオブジェクト指向的な設計を意識したコーディングが可能です。
今回利用するのは主に以下のモジュールです。
Lensのおさらい
Lensを使ったことのある人にはおなじみだと思いますので、特に解説はしません。
type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t -- Lens型(GetterやSetterの一般形) type Lens' s a = Lens s s a a -- Lensの省略形 (^.) :: s -> Getting a s a -> a -- 値をgetする (.~) :: ASetter s t a b -> b -> s -> t -- 値をsetする (%~) :: Profunctor p => Setting p s t a b -> p a b -> s -> t -- 函数を適用したものをsetする (.=) :: MonadState s m => ASetter s s a b -> b -> m () -- 値をMonadStateの文脈でsetする( (.~)のMonadState版 ) (%=) :: (Profunctor p, MonadState s m) => Setting p s s a b -> p a b -> m () -- 函数を適用したものをMonadStateの文脈でsetする( (%~)のMonadState版 ) (<~) :: MonadState s m => ASetter s s a b -> m b -> m () -- アクションを実行した結果をsetする lens :: (s -> a) -> (s -> b -> t) -> Lens s t a b -- getterとsetterからLensをbuildする
特に最後のlens
函数は重要です。例えば適当なdata型をmakeLenses
するとき、何が起こっているかを明示すると
data Hoge a b = Hoge { _foo :: a, _baz :: b } makeLenses ''Hoge -- 以下が生成される foo :: Lens' (Hoge a b) a foo = lens _foo (\h x -> h { _foo = x }) baz :: Lens' (Hoge a b) b baz = lens _foo (\h x -> h { _baz = x })
となります。 lens
に、getter :: Lens' s a -> a
とsetter :: Lens' s a -> a -> Lens' s a
を渡すことで、出来上がるlens getter setter
は上手くLensとして振る舞ってくれます。
型クラスとオブジェクト
Lensでオブジェクトを定義しましょう。
例として、ブロック崩しゲームのようなものを考えてみましょう。ブロック崩しには当然、ボールやバーが必要ですのでこれを定義してみます。
data Ball = Ball { _posBall :: (Int, Int), _velBall :: (Int, Int), _r :: Int } deriving (Eq, Show) makeLenses ''Ball data Bar = Bar { _posBar :: (Int, Int), _velBar :: (Int, Int), _width :: Int, _height :: Int } deriving (Eq, Show) makeLenses ''Bar
posHoge
はオブジェクトの座標にあたります。わざわざ名前を変えているのは、函数の多重定義になってしまうからです。また、オブジェクトは座標を更新して動かしたいので、このようなメソッドを定義します。
Haskellでは、いわゆるメソッドに当たる部分は型クラスのメソッドとして定義します。メソッドはオブジェクトに関する値を戻り値にとる函数というよりもオブジェクト自体を操作する函数であることが多いのでデータ型とは分けて定義するほうが便利です。また、型クラスのメソッドなら、同じ名前でもデータ型によって異なる振る舞いが定義できます。
class Move c where update :: State c () instance Move Ball where update = do (vx,vy) <- use velBall posBall %= (\(x,y) -> (x+vx, y+vy)) instance Move Bar where -- Barは横にしか動けない update = do (vx,_) <- use velBall posBar %= (\(x,y) -> (x+vx,y))
特に意味はないですが、オブジェクトによって函数の振る舞いを変えることが出来ました。
さて、posBall
とposBar
が同じ座標を表しているのに名前が違うのは何かと面倒なので、これも型クラスを使ってまとめてしまいます。
class HasPos c where pos :: Lens' c (Int, Int) instance HasPos Ball where pos = posBall instance HasPos Bar where pos = posBar
はい、これでposBall
とposBar
を区別する必要はありません。これを使えば、pos
をもつ任意の要素に対するメソッドが定義できます。
reset :: (HasPos c) => State c () reset = do pos .= (0,0)
reset
はHasPos
型クラスのインスタンスならなんでも適用できます。つまり、Ball
とBar
はどちらもreset
で操作をすることができるわけです。便利ですね!
Classyとサブクラス
さて、上ではHasPos
を定義して名前の衝突を防ぎましたが、これを共通する全ての函数について行わなければならないのは面倒です。そこで、pos
やvel
などの共通するメンバをもつ抽象クラスを作り、Ball
やBar
はこれらを内部に含む(ある意味ではサブクラス・継承したクラスのようなもの)クラスとして定義することにしましょう。
data GameObj = GameObj { _pos :: (Int, Int), _vel :: (Int, Int) } makeClassy ''GameObj -- 以下が自動生成される class HasGameObj c where gameObj :: Lens' c GameObj instance HasGameObj GameObj where gameObj = id pos :: (HasGameObj c) => Lens' c (Int, Int) vel :: (HasGameObj c) => Lens' c (Int, Int)
makeClassy
はGameObj
からHasGameObj
型クラスを生成します。これによって、HasGameObj
のインスタンスになったものは自由にpos
とvel
を使えるようになります。
どういうことか見てみましょう。
data Ball = Ball { _ballObj :: GameObj, _r :: Int } deriving (Eq, Show) makeLenses ''Ball instance HasGameObj Ball where gameObj = ballObj
これでBall
はHasGameObj
のインスタンスになったので、b :: Ball
に対してb ^. pos
とかb & vel .~ (1,0)
などとかけるようになったわけです。
また、少しデザインパターン的ですが、自分自身を更新する函数を内部に持つオブジェクトの定義も出来ます。
同じ型を持つオブジェクトの実装を変えられるようにしたい -qiita
これは、例えば変な動きをするボールを作りたいときに、ボールの動きをデータの生成時に指定したいみたいな状況で使えると思います。
(上の記事のAutonomie
の実装を参照。auto
を例えばBall
型で、runAuto
をState Ball ()
とすればこのような実装も可能。)
ここまででかなりのことはできるようになったと思います。
今回は記事タイトルの通り、「実践」を意識した解説を行いました。例えば私はChimeraという弾幕シューティングゲームを全てHaskellで実装していますが、Lensはこれくらい本格的なプロジェクトになっても通用するレベルの強力さを秘めています。
Lensは(たぶん)他にも使い方があります。あなたもこの強力なライブラリで快適なHaskellコーディングを楽しみましょう!
さあいますぐインストール! *1
*1:2014/02/02時点でlens-4.0がリリースされています。