Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
49
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

Organization

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

最近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:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
49
Help us understand the problem. What are the problem?