JKになりたい

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

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

Functional Programming in ScalaのExercise6.11の問題をStateモナドを使用して解いてみます。

問題設定

スナックの自動販売機をモデリングする有限状態オートマトンを実装する。

この自動販売機は2種類の入力を受け付ける。
1つは硬貨の投入、もう1つはハンドルを回してスナックを取り出すこと。

自動販売機の状態はロックされたか、ロック解除された状態かのどちらかになる。
ロックが解除された状態でハンドルをまわすとスナックが出てくる。

・ロックされた状態の自動販売機に硬貨を入れると、スナックが残っている場合ロックが解除される
・ロックが解除された状態の自動販売機のハンドルをまわすと、スナックが出てきてロックがかかる
・ロックされた状態でハンドルをまわしたり、ロックが解除された状態で硬貨を投入しても何も起こらない
・スナックが売り切れたとき、自動販売機は入力をすべて無視する

最終的には、入力のリストを渡し、自動販売機の最終的な状態をかえすsimulateMachine関数を実装する。
例えば、自動販売機に10枚のコインと5つのスナックが入っており、4つスナックが購入されたときの自動販売機の最終状態は(14, 1)になる。

解答例

sealed trait Input
case object Coin extends Input
case object Turn extends Input

case class Machine(locked: Boolean, candies: Int, coins: Int)

def inputCoin = State[Machine, (Int, Int)] {
  case machine if machine.candies == 0 => (machine, (machine.coins, machine.candies))
  case machine if machine.locked => {
    (machine.copy(locked = false, coins = machine.coins + 1), (machine.coins + 1, machine.candies))
  }
  case machine => (machine, (machine.coins, machine.candies))
}

def turnHandle = State[Machine, (Int, Int)] {
  case machine if machine.candies == 0 => (machine, (machine.coins, machine.candies))
  case machine if !machine.locked =>  {
    (machine.copy(locked = true, candies = machine.candies - 1), (machine.coins, machine.candies - 1))
  }
  case machine => (machine, (machine.coins, machine.candies))
}

def simulateMachine(inputs: List[Input]): State[Machine, (Int, Int)] = {
  def action(input: Input) = input match {
    case Coin => inputCoin
    case Turn => turnHandle
  }
  inputs.traverse(action).map(_.last)
}

挙動の確認

val initMachine = Machine(
  locked = true,
  candies = 3,
  coins = 0
)

//コインは0->2枚になり、スナックは2個減る
simulateMachine(List(Coin, Turn, Coin, Turn)).run(initMachine).value //(Machine(true,1,2),(2,1))

//コインは1枚目のみ入り、後は無視される
simulateMachine(List(Coin, Coin, Coin)).run(initMachine).value //(Machine(false,3,1),(1,3))

//何もおきない
simulateMachine(List(Turn, Turn, Turn)).run(initMachine).value //(Machine(true,3,0),(0,3))

//最後のCoin->Turnの段階で、スナックが0個なので以降の入力は無視される
simulateMachine(List(Coin, Turn, Coin, Turn, Coin, Turn, Coin, Turn)).run(initMachine).value //(Machine(true,0,3),(3,0))

スナックが0のときと、ロックされた状態でハンドルをまわしたり、ロックが解除された状態で硬貨を投入したときのコードがinputCoinとturnHandleで冗長になってしまっている、愚直な実装です。

コイン投入の時の挙動と、ハンドルをまわした時の挙動を独立して考える方が、はじめて実装する段階ではわかりやすいと思います。

また、traverseでstateモナドを合成するとvalueが蓄積されてしまうので、うまくスタックを積み上げないような実装が出来ればよかったです。

本家の解答はよりスマートなので、そちらも参照ください。 fpinscala/State.scala at master · fpinscala/fpinscala · GitHub