JKになりたい

何か書きたいことを書きます。主にWeb方面の技術系記事が多いかも。

CatsのStateモナドを使ってFP in ScalaのEx6.11 自動販売機問題を解く (1)Stateモナドについて

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モナドを用いて実装していくのですが、、長くなったので次の記事にまわします・・。