Stateモナドについてざっくりと。
状態の変化を伴うミュータブルな計算をFunctional Programmingのコンテキストで表現可能にしたもの。
catsにおけるStateモナドのapplyメソッドのシグネチャは以下のようになっています
def apply[S, A](f: S => (S, A)): State[S, A]
見たとおり、状態Sを受け取って、(S, A)を返す関数を渡します。
これだけではさっぱりなので、使用例を考えていきます。。
例に示すのは乱数を生成する関数です。
乱数生成のためのオブジェクトは状態を持つため、以下は典型的な参照透過性のない関数です。
def createRandomInt = { val random = new Random() random.nextInt() }
これを少し改良してみましょう。
def createRandomInt(random: Random) = random.nextInt()
これで同じRandomオブジェクトに対しては同じ出力結果が帰るようになったので、先程の例よりかはかなりテストしやすくなりました。
ただ、これも非常に辛い点が幾つかあります。
というのは、同じ乱数ジェネレータを再現するには、Randomオブジェクトを同じシードで生成した上に、同じ回数この関数の呼び出しをしないといけません。(副作用により状態を作り出す必要がある)
以前の状態はこの関数を呼び出すと完全に失われてしまいます。
これを、Stateモナドを使って参照透過性のある実装にしてみます。
状態を副作用として更新するのではなく、生成された値と共に新しい状態を返すようにします。
また、scala.util.randomはシングルトンオブジェクトになっており、nextIntを呼ぶと必ず副作用が生じるので、線形合同法により乱数を生成するようにします。
実装はFP in Scalaのリスト6-3を参考にしました。
type SEED = Long def createRandomInt = State[SEED, Int] { state => val nextState = (state * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL val n = (nextState >> 16).toInt (nextState, n) }
これは何度呼び出しても元のstateが変わる事がない、つまり副作用がない関数になっています。
createRandomInt.run(1).value._2 //384748 createRandomInt.run(1).value._2 //384748 createRandomInt.run(1).value._2 //384748
また、帰ってくる次のstateを使用して、新たな乱数を生成することができます。
val s = for { s1 <- createRandomInt s2 <- createRandomInt s3 <- createRandomInt } yield (s1, s2, s3) s.run(1).value //(245470556921330,(384748,-1151252339,-549383847))
val s = for { s1 <- createRandomInt s2 <- createRandomInt s3 <- createRandomInt } yield s3 s.run(1).value //(245470556921330,-549383847)
やや解釈が難しいコードかもしれないですが、これでnexrStateを使用して関数を合成できています。 帰ってきたタプルの1要素が最後に帰ってきたstateで、2要素が生成された乱数です。
また、catsのStateモナドには便利メソッドとして、get,set,inspect,modify等があります。
get[S]はState[S, S]を生成します
val state = State.get[Int] state.run(1).value //(10, 10)
set(s: S)はState[S, Unit]を生成します。 つまり、一度ためておいたvalueをすべて破棄して、状態だけ保持することができます。
val state = State.set(1) state.run(10).value //(10, ())
inspectは次の状態から値を生成することができます。
val state1 = createRandomInt state1.run(1).value #(25214903928,384748) val state2 = createRandomInt.inspect(i => s"hello: $i") state2.run(1).value #(25214903928,hello: 25214903928)
state2を見ると、nextStateであるSEED「25214903928」からvalueを生成していることがわかります。
modifyは状態を変化させることができます。
val state1 = createRandomInt state1.run(1).value #(25214903928,384748) val state2 = createRandomInt.modify(_ + 1) state2.run(1).value #(25214903929,384748)
state2をみると、modifyを使用していないstate1に比べて、stateであるSEEDの数が + 1されていることがわかります。
ここまで、Stateモナドについてざっくり書きました。
ここから、本題である自動販売機のモデルをStateモナドを用いて実装していくのですが、、長くなったので次の記事にまわします・・。