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

Just $ A sandbox

プログラミングと計算機科学とかわいさ

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 -> asetter :: 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))

特に意味はないですが、オブジェクトによって函数の振る舞いを変えることが出来ました。
さて、posBallposBarが同じ座標を表しているのに名前が違うのは何かと面倒なので、これも型クラスを使ってまとめてしまいます。

class HasPos c where
  pos :: Lens' c (Int, Int)

instance HasPos Ball where
  pos = posBall
instance HasPos Bar where
  pos = posBar

はい、これでposBallposBarを区別する必要はありません。これを使えば、posをもつ任意の要素に対するメソッドが定義できます。

reset :: (HasPos c) => State c ()
reset = do
  pos .= (0,0)

resetHasPos型クラスのインスタンスならなんでも適用できます。つまり、BallBarはどちらもresetで操作をすることができるわけです。便利ですね!

Classyとサブクラス

さて、上ではHasPosを定義して名前の衝突を防ぎましたが、これを共通する全ての函数について行わなければならないのは面倒です。そこで、posvelなどの共通するメンバをもつ抽象クラスを作り、BallBarはこれらを内部に含む(ある意味ではサブクラス・継承したクラスのようなもの)クラスとして定義することにしましょう。

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)

makeClassyGameObjからHasGameObj型クラスを生成します。これによって、HasGameObjインスタンスになったものは自由にposvelを使えるようになります。
どういうことか見てみましょう。

data Ball = Ball { _ballObj :: GameObj, _r :: Int } deriving (Eq, Show)
makeLenses ''Ball

instance HasGameObj Ball where
  gameObj = ballObj

これでBallHasGameObjインスタンスになったので、b :: Ballに対してb ^. posとかb & vel .~ (1,0)などとかけるようになったわけです。

また、少しデザインパターン的ですが、自分自身を更新する函数を内部に持つオブジェクトの定義も出来ます。
同じ型を持つオブジェクトの実装を変えられるようにしたい -qiita

これは、例えば変な動きをするボールを作りたいときに、ボールの動きをデータの生成時に指定したいみたいな状況で使えると思います。
(上の記事のAutonomieの実装を参照。autoを例えばBall型で、runAutoState Ball ()とすればこのような実装も可能。)

ここまででかなりのことはできるようになったと思います。
今回は記事タイトルの通り、「実践」を意識した解説を行いました。例えば私はChimeraという弾幕シューティングゲームを全てHaskellで実装していますが、Lensはこれくらい本格的なプロジェクトになっても通用するレベルの強力さを秘めています。

Lensは(たぶん)他にも使い方があります。あなたもこの強力なライブラリで快適なHaskellコーディングを楽しみましょう!
さあいますぐインストール! *1

*1:2014/02/02時点でlens-4.0がリリースされています。