Home About
Haskell , Monad

Haskell / ポケモンのモデル化とその進化 Monad による連続変換処理(その2)

前回 ポケモンのモデル化とその進化を Maybe モナドと bind を使って記述しました。 変換処理を連続で適用するという意味では、前回の段階では、コードが読みやすくなった程度で モナドを使う利点がさほどない状態でした。 今回は、ポケモン進化モデルのルールを追加し、モナドを使ううれしさを探ってみます。

Step1 キャタピー進化をモデル化(改訂版)

キャタピーのレベルに応じた進化を考えてみます。 設定は以下のようにします。

と、ここまでが前回と同じルールです。さらに以下のルールを追加します。

それでは、これをコードにします:

data Pokemon = Caterpie Int | Metapod Int | Butterfree Int

instance Show Pokemon where
  show pokemon = toString pokemon where
                      toString (Caterpie l)   = "Caterpie Level=" ++ (show l) 
                      toString (Metapod l)    = "Metapod Level=" ++ (show l) 
                      toString (Butterfree l) = "Butterfree Level=" ++ (show l) 


-- トレーニングとバトル
train :: Pokemon -> Pokemon
train (Caterpie l)
    | (l+1)>=5 = Metapod $ l+1
    | otherwise = Caterpie $ l+1
train (Metapod l)
    | (l+1)>=10 = Butterfree $ l+1
    | otherwise = Metapod $ l+1
train (Butterfree l) = Butterfree $ l+1

fight :: Pokemon -> Pokemon
fight (Caterpie l)
    | (l+3)>=5 = Metapod $ l+3
    | otherwise = Caterpie $ l+3
fight (Metapod l)
    | (l+3)>=10 = Butterfree $ l+3
    | otherwise = Metapod $ l+3
fight (Butterfree l) = Butterfree $ l+3


-- トレーニングとバトルのモナド版
trainM :: Pokemon -> Maybe Pokemon
trainM p = Just (train p)

fightM :: Pokemon -> Maybe Pokemon
fightM p
    | even $ level (fight p) = Nothing
    | otherwise              = Just (fight p)
    where
        level :: Pokemon -> Int
        level (Caterpie l)   = l
        level (Metapod l)    = l
        level (Butterfree l) = l

-- ポケモンゲットだぜ
getPokemon :: Pokemon
getPokemon = Caterpie 1

getPokemonM :: Maybe Pokemon
getPokemonM = Just getPokemon

これを GHCi にロードしてプレイしてみます:

$ ghci
> :load pokemon.hs
> myPokemonM = getPokemonM
> myPokemonM
Just Caterpie Level=1

ポケモン(モナド版)をゲットして確認しました。レベル1のキャタピーを手にいれています。 続いてトレーニングしたりバトルしてみます:

> myPokemonM >>= trainM
Just Caterpie Level=2
> myPokemonM >>= trainM >>= fightM
Just Metapod Level=5
> myPokemonM >>= trainM >>= fightM >>= fightM
Nothing

おっと。戦闘不能になった。何が起きた?

ということです。

もう少しいくつかプレイしてみます。

> myPokemonM >>= fightM >>= trainM >>= trainM
Nothing
> myPokemonM >>= fightM >>= trainM
Nothing
> myPokemonM >>= fightM
Nothing

ゲットしてすぐバトルするとあと何をしても Nothing です。 つまり、最初に fightM したときに Nothing になっている。

このようにモナドで bind していくと、連続変換中のどこで Nothing になろうがとりあえずコードは続けて書くことができる、という特徴があります。

もし、ゲットしたキャタピーがレベル 2 だった場合を考えてみましょう。

> myAnotherPokemonM = Just $ Caterpie 2
> myAnotherPokemonM
Just Caterpie Level=2

このポケモン myAnotherPokemonM で試しましょう。

> myAnotherPokemonM >>= fightM >>= trainM >>= trainM
Just Metapod Level=7

先ほどと育成方法は同じ(バトル トレーニング トレーニング)ですが、今度は Nothing ではなく、トランセル(Metapod)レベル 7 になりました。

せっかくなので、バタフリーまで進化させましょう。

> myPokemonM >>= trainM >>= fightM >>= trainM
Just Metapod Level=6
> myPokemonM >>= trainM >>= fightM >>= trainM >>= fightM
Just Metapod Level=9
> myPokemonM >>= trainM >>= fightM >>= trainM >>= fightM >>= trainM
Just Butterfree Level=10

うまくバタフリーまで進化できました。

一度ここでコードを整理します。 先ほどうまくバタフリーまで進化できた育成方法は takeCare 関数にしておきます。

takeCare :: Maybe Pokemon -> Maybe Pokemon
takeCare p = p >>= trainM >>= fightM >>= trainM >>= fightM >>= trainM

コード全体 pokemon.hs:

data Pokemon = Caterpie Int | Metapod Int | Butterfree Int

instance Show Pokemon where
  show pokemon = toString pokemon where
                      toString (Caterpie l)   = "Caterpie Level=" ++ (show l) 
                      toString (Metapod l)    = "Metapod Level=" ++ (show l) 
                      toString (Butterfree l) = "Butterfree Level=" ++ (show l) 


train :: Pokemon -> Pokemon
train (Caterpie l)
    | (l+1)>=5 = Metapod $ l+1
    | otherwise = Caterpie $ l+1
train (Metapod l)
    | (l+1)>=10 = Butterfree $ l+1
    | otherwise = Metapod $ l+1
train (Butterfree l) = Butterfree $ l+1


fight :: Pokemon -> Pokemon
fight (Caterpie l)
    | (l+3)>=5 = Metapod $ l+3
    | otherwise = Caterpie $ l+3
fight (Metapod l)
    | (l+3)>=10 = Butterfree $ l+3
    | otherwise = Metapod $ l+3
fight (Butterfree l) = Butterfree $ l+3


-- トレーニングとバトルのモナド版
trainM :: Pokemon -> Maybe Pokemon
trainM p = Just (train p)

fightM :: Pokemon -> Maybe Pokemon
fightM p
    | even $ level (fight p) = Nothing
    | otherwise              = Just (fight p)
    where
        level :: Pokemon -> Int
        level (Caterpie l)   = l
        level (Metapod l)    = l
        level (Butterfree l) = l


-- 育成 
takeCare :: Maybe Pokemon -> Maybe Pokemon
takeCare p = p >>= trainM >>= fightM >>= trainM >>= fightM >>= trainM


-- ポケモンゲットだぜ
getPokemon :: Pokemon
getPokemon = Caterpie 1

getPokemonM :: Maybe Pokemon
getPokemonM = Just getPokemon


myPokemonM = getPokemonM

GHCi で確認:

> :reload
> takeCare myPokemonM
Just Butterfree Level=10

もし、レベル 2 のキャタピーで育成をはじめたらどうなる?

> myAnotherPokemonM = Just $ Caterpie 2
> takeCare myAnotherPokemonM
Nothing

戦闘不能(Nothing)になりました。

どの段階で Nothing になったのだろうか?

Writer モナドを使う

Maybe モナドなポケモンを takeCare すると、うまくバタフリーまで進化できたり、戦闘不能(Nothing)になったりします。

どの段階で進化したり、戦闘不能になったのでしょうか。 これをわかるようにすることはできないのでしょうか?

もしJavaなど記述した場合は、コードの途中に適宜 System.out.println などをすればよいわけです(一つの方法としては)。 Haskell でそのような処理をするには Writer モナドを使うとよいらしい。 試してみます。

(書きかけです)

Liked some of this entry? Buy me a coffee, please.