最近HaskellでFlappyBirdを作っているのですが今までどう使っていいかわからなかったStateモナドをうまく使えた気がしたのでqiitaに残しておこうと思います。
Stateモナドは状態を扱える文脈とかReaderとWriterの随伴から出てくるとかそういうやつです。mtlに入ってるStateを使って簡単に説明してみます。
import Control.Monad.State
add1 :: State Int ()
add1 = do
n <- get
put (n + 1)
main = print $ execState add1 0
add1
は単に1を足す関数です。ただしStateモナドを使って実装されていて詳細を見てみると
- 状態から数を取り出して
- 1を足して状態に詰め直す
というようなプログラムになっています。
こんな簡単な例をみても何も良さがわからないと思うので今回自分が出会った便利に使える例を紹介したいです。
FlappyBirdは鳥がふわふわ飛ぶゲームなので鳥を表すオブジェクトが必要になります。
data Bird = Bird {
pos :: (Double, Double)
, speed :: (Double, Double)
}
キーボードが押されたら上に飛び、そうじゃない時は自由落下するように鳥の位置を更新する関数を考えましょう
move :: Bool -> Bird -> Bird
move pressed bird = let (posX, posY) = pos bird
(speedX, speedY) = speed bird
speedY' = if pressed then 10 else speedY - 0.1
posX' = posX + speedX
posY' = posY + speedY'
in bird {pos = (posX', posY'), speed = (speedX, speedY')}
書いてみて思ったのですがまぁ悪くはないですね。これでもいいのですがStateモナドを使うともうちょっとわかりやすくかけます
getPosX :: State Bird Double
getPosX = get >>= return . fst . pos
setPosX :: Double -> State Bird ()
setPosX x = do
bird <- get
let (_, y) = pos bird
put $ bird {pos = (x, y)}
getPosY :: State Bird Double
getPosY = get >>= return . snd . pos
setPosY :: Double -> State Bird ()
setPosY y = do
bird <- get
let (x, _) = pos bird
put $ bird {pos = (x, y)}
getSpeedX :: State Bird Double
getSpeedX = get >>= return . fst . speed
setSpeedX :: Double -> State Bird ()
setSpeedX x = do
bird <- get
let (_, y) = speed bird
put $ bird {speed = (x, y)}
getSpeedY :: State Bird Double
getSpeedY = get >>= return . snd . speed
setSpeedY :: Double -> State Bird ()
setSpeedY y = do
bird <- get
let (x, _) = speed bird
put $ bird {speed = (x, y)}
まずはStateモナドでBirdを操作するための準備コードです。単純なコードですがこれを毎回用意するのは大変ですね… いったん目をつぶって新しいmove'
関数を実装してみましょう。
move' :: Bool -> State Bird ()
move' pressed = do
if pressed
then setSpeedY 10
else do
speedY <- getSpeedY
setSpeedY $ speedY - 0.1
posX <- getPosX
posY <- getPosY
speedX <- getSpeedX
speedY <- getSpeedY
setPosX $ posX + speedX
setPosY $ posY + speedY
move
より少し長くなりましたが読みやすくもなりました。
以下のように実行して同じ動作をするか比較してみましょう
main = do
let bird = Bird {pos = (0,0), speed = (1,1)}
print $ move False bird
print $ execState (move' False) bird
Stateを使ったほうが読みやすくはなったわけですが専用の関数を用意するのは大変ですよね。ところで用意した関数は全部getterとsetterでしたよね。HaskellでアクセッサといえばLensですがLensを使うと今までのコードがとても短く簡単に書くことが出来ます。
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
import Control.Monad.State
data Bird = Bird {
_pos :: (Double, Double)
, _speed :: (Double, Double)
} deriving Show
makeLenses ''Bird
move :: Bool -> State Bird ()
move pressed = do
if pressed
then speed._2 .= 10
else speed._2 -= 0.1
(speedX, speedY) <- use speed
pos._1 += speedX
pos._2 += speedY
main = do
let bird = Bird {_pos = (0,0), _speed = (1,1)}
print $ execState (move False) bird
どうでしょうか
lens
ライブラリにはもともとStateで使える演算子が定義してあるので自分で専用の関数を特に用意すること無く使うことができます。複雑なデータ構造の状態を書き換える処理を書くときはStateモナドとLensの組み合わせがとても便利だという話でした