新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。もう新人じゃないかも…

学習記録:Scala関数型デザイン 第6章

第6章:純粋関数型の状態(続き)

状態アクションデータ型の一般化

前節で記述したunit, map, map2, flatMapなどはどの角度からも乱数ジェネレータに特化しているとは言いにくい。
これらは状態アクションを処理するための汎用目的の型であり,状態の型を特別扱いしない。
たとえば,mapにはRNGの状態アクションを処理しているという認識はないので,より汎用的なシグネチャを与えることができる。

def map[S,A,B](a: S => (A, S))(f: A => B): S => (B, S)

シグネチャを上のように変更したとしても,mapの実装は変更する必要がない。
任意の型の状態を処理するには,Randよりも汎用的な型が必要になる。

type State[S,+A] = S => (A, S)

このStateは,何らかの状態を扱う計算,あるいは状態アクション,状態遷移の省略形である。
これをクラスとして独立させ,関数を追加すると良い。

case class State[S,+A](run: S => (A, S)) {
  def map[B](f: A => B): State[S, B] = ???
  def map2[B,C](sb: State[S, B])(f: (A, B) => C): State[S, C] = ???
  def flatMap[B](f: A => State[S, B]): State[S, B] = ???
}

あとは,RandをStateの型エイリアスにすれば良い。

  type Rand[A] = State[RNG, A]
Exercise 6.10
  • unit, map, map2, flatMapを一般化し,Stateケースクラスのメソッドとして追加せよ。

この辺はよくわからず・・・後で見返す。
とりあえずunit。こちらはコンパニオンオブジェクトに実装する。

object State {
  type Rand[A] = State[RNG, A]

  def unit[S, A](a: A):	State[S, A] =
    State(s => (a, s))
}

続いてflatMap, map, map2。こちらはケースクラスに記述。

case class State[S,+A](run: S => (A, S)) {
  def map[B](f: A => B): State[S, B] =
    flatMap(a => unit(f(a)))
  def map2[B,C](sb: State[S, B])(f: (A, B) => C): State[S, C] =
    flatMap(a => sb.map(b => f(a, b)))
  def flatMap[B](f: A => State[S, B]): State[S, B] =
    State(s => {
      val (a, s1) = run(s)
      f(a).run(s1)
    })
}

純粋関数型の命令型プログラミング

ここまでは,明確なパターンに従う関数を記述してきた。
それらは状態アクションを実行し,valに結果を代入し,
そのvalを利用する別の状態アクションを実行し,その結果を別のvalに代入する・・といったものだった。
これは命令型プログラミングによく似ている。

命令型プログラミングのパラダイムでは,プログラムはステートメントの連なりであり,
ステートメントはそれぞれプログラムの状態を変化させる可能性がある。
それはまさに本章で行ってきたことだが,ここでの「ステートメント」はStateのアクションで,実際には関数である。
それらは関数として,引数として渡された現在のプログラムの状態を読み取り,単に値を返すことでプログラムの状態を書き出す。

本章では,ステートメントからステートメントへの状態の伝達を処理するために,
map, map2, flatMapなどのコンビネータを実装してきた。
しかし,その過程で命令型のスタイルが少し失われてしまったように思える。

以下の例について見てみる。Rand[A]はState[RNG, A]の型エイリアスである。

val ns: Rand[List[Int]] =
  int.flatMap(x =>        // intはランダムな整数を一つ生成するRand[Int]型の値
    int.flatMap(y =>      // 同上
      ints(x).map(xs =>  // ints(x)は長さxのリストを生成
        xs.map(_ % y))))  // リスト内全ての要素をyで割った余りと置き換える

これでは何が行われているのかよくわからない。
ただし,mapとflatMapは定義済みなので,for内包表記を使って命令型のスタイルを取り戻すことができる。

val ns: Rand[List[Int]] = for {
  x <- int   // 整数xを生成
  y <- int   // 整数yを生成
  xs <- ints(x)  // 長さxのリストxsを生成
} yield xs.map(_ % y)  // 各要素をyで割った余りと置き換えたリストxsを返す

このコードの方がはるかに読み書きしやすく,わかりやすい。
この種の命令型プログラミングでfor内包表記を利用するためには,Stateの2つのプリミティブコンビネータが必要になる。
1つは状態を読み取るためのgetコンビネータ,もう1つは状態を書き出すためのsetコンビネータである。

def modify[S](f: S => S): State[S, Unit] = for {
  s  <- get  // 現在の状態を取得しsに代入
  _ <- set(f(s)) // sにfを適用し,新しい状態を設定
} yield *(

このメソッドは,関数fから渡された状態を更新するStateアクションを返す。
このアクションは,状態以外に戻り値がないことを示すUnitを生成する。
では,getアクションとsetアクションはどのようなものになるか。

def get[S]: State[S, S] = State(s => (s, s))

getアクションは,渡された状態を値として返すだけである。

def set[S](s: S): State[S, Unit] = State(_ => ((), s))

setアクションは,新しい状態sを使って構築される。
結果として得られるアクションは,渡された状態を新しい状態と置換え,()を返す。

これら2つの単純なアクションと,ここまで記述してきたStateのコンビネータさえあれば,
あらゆる状態の状態機械あるいはステートフルプログラムを純粋関数方式で実装できる。

まとめ

本章では,状態を持つ純粋関数型のプログラムを記述する方法について述べた。
取っ掛かりとして乱数の生成の例を取り上げたが,このパターンは様々なドメインで発生する。
考え方は単純で,引数として状態を受け取る純粋関数を使用し,結果とともに新しい状態を返す。
次回,副作用に依存する命令型APIに遭遇したときは,その純粋関数型バージョンを作成し,
本章で記述した関数を使ってその処理をもっと便利なものにできるかどうか考えてみてほしい。