Blogaomu

WEBアプリケーション開発とその周辺のメモをゆるふわに書いていきます。

『Scala関数型デザイン&プログラミング』第6章のメモ

Scala関数型デザイン&プログラミング』(初版)の第6章を読んで初見ではいまいち理解できなかった部分について自分なりに理解を試みたメモです。第6章の中でも特に最後の自動販売機の問題(EXERCISE 6.11)については解答を見ても何をやっているか、どのような意図があるのか理解できませんでした。他にこんな考え方や解釈もあるというご意見があればコメントで教えていただきたいです。

State

第6章では State データ型が紹介されています。このデータ型は S => (A, S) という関数が本質で、ある状態(S)を元に何かを行いその結果(A)および新しい状態(S)を返すことを表現しています。書籍では乱数の例で説明されていますが、このような関数を連結したり値を変換したりして最終的な結果や状態を取得するような用途に向いていると理解しました。その際に状態を次の関数に引き渡すことが煩わしくなります。 State はこの煩わしさを閉じこめているので利用者にとって扱いやすいコードが書けるようになっています。

// 状態を次の関数に引き渡す煩わしさの例
def randomPair(rng: RNG): ((Int, Int), RNG) = {
  val (i1, rng2) = rng.nextInt
  val (i2, rng3) = rng2.nextInt // 誤ってrng.nextIntにすると、i2はi1と同じ値になってしまう
  ((i1, i2), rng3) // ここも誤ってrng2を返すと、別の箇所でnextIntを呼ぶ場合i2と同じ値を生成してしまう
}
// Stateを使うと...
val nextInt: State[RNG, Int] = State(rng => rng.nextInt)

// 途中の状態を引き渡すことを記述せずに表現できる
// その辺はStateのmapやflatMapがいい感じにやってくれている
def randomPair: State[RNG, (Int, Int)] = for {
  i1 <- nextInt
  i2 <- nextInt
} yield (i1, i2)

自動販売機の問題

Photo 3922542 © Crystal Srock | Dreamstime.com

EXERCISE 6.11は一度自分で考えてみてコードを書いたのですが、人に説明する際にあまりしっくりきませんでした(型を合わせに行ったような実装になった)。そこで解答を見たのですが、とある部分が全く分からんとなり改めて何をやっているのだろうかと考えてみました。

自動販売機について

まず自動販売機について整理します。

  • 自動販売機(Machine)はロックされているかどうか(locked)、お菓子の個数(candies)、硬貨の個数(coins)という状態を持つ
  • 入力(Input)という概念がある
    • 「硬貨を投入する(Coin)」「ハンドルを回す(Turn)」という自動販売機に対する操作
  • 自動販売機にはいくつかのルールがある
  • 一連の入力を受け付けて自動販売機の状態が最終的にどうなるかをシミュレーションしたい(simulateMachineメソッド)

これは余談ですが、本文では「単純なスナックの自動販売機をモデリングする」と書かれています。これを読んで、お菓子やパンが並び、硬貨を入れて番号を押すと機械によって押し出されるようなものを私は最初に想像していました。しかし、ハンドルを回すという特徴的な操作でピンと来まして、上に貼った画像のような丸いガムが出てくるやつを思い浮かべるのがより適していると分かりました。gumball machineとかgumball vending machineと呼ばれているそうです。

自動販売機の状態遷移

さて、自動販売機のシミュレーター実装の話に戻ります。ここからは先ほどの解答を見た上で理解したことを書いていきます。

1つ目のポイントは自動販売機の状態遷移です。先ほど整理した通り、自動販売機の状態は以前の自動販売機の状態と入力によって決定します。なので (Machine, Input) => Machine という関数で表すことができそうです。

MachineState で扱うことを考えると、State.modify[S](f: S => S): State[S, Unit] を使って状態を更新できると良い感じになりそうです。しかし、modify に渡す関数は S => S で引数と返り値が同じ型という制約があり、update の型とはマッチしません。そこで (Machine, Input) => Machine をカリー化して Input => Machine => Machine という関数にすると、最初の引数に Input 型の値を渡すことで Machine => Machine の関数を得られます。これを modify に渡せば State[Machine, Unit] の値が返ってきます。

ここまでをコードにするとこんな感じになります。

// 最初に考えたupdate
def update(input: Input, machine: Machine): Machine = ???

// 実装は省略
def update: Input => Machine => Machine = ???

// この時点では単一のinputを渡すことを考える
def simulateMachine(input: Input): State[Machine, (Int, Int)] = {
  State.modify[Machine](update(input)) // => State[Machine, Unit]
  ???
}

自動販売機シミュレーター

2つ目のポイントはシミュレーターで自動販売機の硬貨とお菓子の数を得る処理です。これらの数は自動販売機から取得できます。State の状態を取得するには State.get[S]: State[S, S] を使うと良さそうです。get で得られた値としての状態を map で変形すれば期待するものが返ってきます。

def update: Input => Machine => Machine = ???

// この時点では単一のinputを渡すことを考える
def simulateMachine(input: Input): State[Machine, (Int, Int)] = {
  State.modify[Machine](update(input)) // => State[Machine, Unit]
    .flatMap(_ => State.get)           // => State[Machine, Machine]
    .map(s => (s.coins, s.candies))
}

// forを使って命令的な書き方に置き換える
def simulateMachine(input: Input): State[Machine, (Int, Int)] = for {
  _ <- State.modify[Machine](update(input)) // => State[Machine, Unit]
  s <- State.get                            // => State[Machine, Machine]
} yield (s.coins, s.candies)

// 動かしてみる
// 最初は硬貨を投入するシミュレーション
// 次は、その状態を使ってハンドルを回すシミュレーション
// 初期状態と比較すると硬貨が1増えて、お菓子が1減る
val machine = Machine(locked = true, candies = 10, coins = 5)
val ((coins, candies), machine2) = simulateMachine(Coin).run(machine)
println(s"coins: ${coins}, candies: ${candies}")
// => coins: 6, candies: 10

val ((coins2, candies2), machine3) = simulateMachine(Turn).run(machine2)
println(s"coins: ${coins2}, candies: ${candies2}")
// => coins: 6, candies: 9

入力のリストを扱う

ここまでで単一の入力について考えられたので、入力のリストを渡すことについて考えたいです。 State には sequence[S, A](sas: List[State[S, A]]): State[S, List[A]] という関数が用意されていて「State のリスト」を「結果がリストになる単一の State」に変換できます。先ほどのステップで Input を渡せば State[Machine, Unit] を作れるようになっていたので List[Input]map すれば List[State[Machine, Unit]] に変換できて sequence に渡せそうです。

def simulateMachine(inputs: List[Input]): State[Machine, (Int, Int)] = for {
  _ <- State.sequence(inputs.map(input => State.modify[Machine](update(input)))) // => State[Machine, List[Unit]]
  s <- State.get                                                                 // => State[Machine, Machine]
} yield (s.coins, s.candies)

// 動かしてみる
// 硬貨を投入してハンドルを回す入力を3回繰り返して、最後にハンドルを回す
// 硬貨は3増えて、お菓子が3減る
val machine = Machine(locked = true, candies = 10, coins = 5)
val inputs: List[Input] = List(Coin, Turn, Coin, Turn, Coin, Turn, Turn)
val ((coins, candies), machine2) = simulateMachine(inputs).run(machine)
println(s"coins: ${coins}, candies: ${candies}")
// => coins: 8, candies: 7

パッと分からなかった記述について

最後に解答を見てパッと分からなかったことについて見ていきます。先ほどのコードで State.sequence が出てくる部分は解答を見ると以下のようになっています。

// import State._ しているので State. は省略している
_ <- sequence(inputs map (modify[Machine] _ compose update))

特に inputs map (modify[Machine] _ compose update) の部分がパッと見た時に何を意味しているのか分かりませんでした。まず map に渡されているブロックから見ていくと composeFunction1 のメソッドで関数を合成します(IntelliJ IDEAで型を確認した)。

www.scala-lang.org

このため、まとまりとしては (modify[Machine] _) compose (update) となります。気になるのが modify[Machine] _ の部分で、これはメソッドを値化しているようです。つまり modify[Machine]Machine => Machine を引数に取って State[Machine, Unit] を返すメソッドなので、値化することで (Machine => Machine) => State[Machine, Unit] というシグネチャの関数となります(注:値化について技術的な詳細については理解できていません)。これによって関数合成ができるようになり、まとまりとしては Input => State[Machine, Unit] の関数となります。

stackoverflow.com

この関数を inputs map に渡しているので List[Input] から List[State[Machine, Unit] に変換でき、sequence に渡せるようになります。

感想

難問というだけあって理解するのに時間が掛かりました。なるほどと思った点は、自動販売機の状態遷移で自動販売機の状態と入力が必要なところを先に入力だけ適用してあとは自動販売機の状態を渡せば決定するというように変換したことでした。これで上手く State として扱えるようになって凄いと思いました。

参考資料

解答例 github.com

書籍内のStateの実装とは異なりますが思想としてはほぼ同じなので、概念を理解する上で参考になりました。 typelevel.org