LoginSignup
59
50

More than 5 years have passed since last update.

Stateモナドが便利に使えた!

Posted at

最近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の組み合わせがとても便利だという話でした :cat2:

59
50
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
59
50