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 モナドを使うとよいらしい。 試してみます。

(書きかけです)