この記事では、Scala 3と関数型ライブラリであるCatsを時折使いつつ、Stateモナドを利用することでmap結果にうるおいを与えられるという話題を紹介します。
- よくある処理: mapしてからペアにする
- まあまあよくある: メソッドはたまに値返さないことがある
- まあまあよくある: メソッドが非同期の場合もある
- Stateモナド
- 通常の関数をStateに持ち上げる
- Stateでmapする
- StateT登場!!
- 一般化する
- まとめ
プログラミング言語で最も頻繁に使われるデータ構造といえば、タプル、とりわけ2つの要素を持つタプルであるペアかもしれない。
【タプルとは?】
タプルとは、いくつかの型を並べて組にし、1つの型として扱えるようにしたものです。例えば、
String
とInt
をくっつけた(String, Int)
というタプルを考えることができます。 タプルを構成する型の数によって、3-タプル、5-タプルといった呼び方をします。特に2-タプルのことをダブルとかペアと呼びます。例えば、2次元座標上の一点を表現するために
(Double, Double)
というペアを使うことができますね。そして、その座標をさらにペアにするとベクトルを作ることができます。
関数型言語の愛好家でなくても、様々な言語でペアを使った処理が行なわれる。
例えば我々が大好きな辞書構造は、辞書のキーと値とのペアが、(内部構造を抜きにして考えれば)リストをなしているもの、として理解することもできる。
Map[K, V] ≒ List[(K, V)]
実際に、このような変換をサポートするプログラミング言語も存在する。例えばScalaでは、ペアのリストをMap
に変換できる:
// Scalaで、x -> yは(x, y)のエイリアス scala> val xs = Seq("foo" -> 42, "bar" -> 100) val xs: Seq[(String, Int)] = List((foo,42), (bar,100)) scala> xs.toMap val res0: Map[String, Int] = Map(foo -> 42, bar -> 100)
このように、ペアは2つの型を組にして1つにするという非常にシンプルな構造でありながら、またその柔軟さゆえに、様々な用途で利用されている。
よくある処理: map
してからペアにする
ペアが登場する非常によくある処理の一例として、あるリストを処理した結果を、処理前のデータから引ける辞書構造にする、というものがある。
例えば、IDをもとにデータベースにアクセスし、その結果を「IDをキーとした辞書構造」にまとめる、という処理は誰もがやったことがあると思う:
// DBからユーザ情報を取得するメソッド def findUserById(id: Int): User = ??? // ユーザID val xs: List[Int] = List(1, 2, 3, 4, 5) // 単純にmapするとUserのリストになるが…… xs.map(findUserById) // => List(User(...), ...) // idとペアにしながらmapすると便利 xs.map(id => id -> findUserById(id)) // => List(1 -> User(...), 2 -> User(...), ...) // Mapにもできる xs.map(id => id -> findUserById(id)).toMap // => Map(...)
辞書にしておくことで後からさらに加工しやすくなって便利だ。
まあまあよくある: メソッドはたまに値返さないことがある
DBが常に値を返すとは限らない。IDに対応するレコードがないかもしれないし、DBの具合が悪いかもしれない。
findUserById
の返り値の型をOption[User]
にして、現実により近づけてみよう:
// DBからユーザ情報を取得するメソッド def findUserById(id: Int): Option[User] = ??? // ユーザID val xs: List[Int] = List(1, 2, 3, 4, 5) xs.map(findUserById) // => List(Some(User(...)), None, ...) // idとペアにしながらmapすると便利 val fxs = xs.map(id => id -> findUserById(id)) // => List(1 -> Some(User(...)), 2 -> None, ...)
しかしこれではfxs
の型がList[(Int, Option[User])]
という微妙に使いづらい型になってしまった。なんとかならないだろうか?
アドホックなパッチとして、Option
型にはmap
メソッドが生えているので、値があるときだけペアになるように修正できる:
val fxs = xs.map(id => findUserById(id).map(id -> _)) // => List(Some(1 -> User(...)), None, ...)
とりあえず動作する形にはできたが、何がどうなってるのか分かりづらくなってしまった。やりたいことは入力と結果をペアにしたいだけなのに……。
まあまあよくある: メソッドが非同期の場合もある
(ここはIO型が分かっている人だけ読めばよいコーナー)
さっきはOption
型を使っていたが、そもそもDBアクセスは副作用があって非同期に動作する可能性があるので、Cats EffectのIO
型でラップされた結果を返すことがある:
import cats.effect.IO def findUserById(id: Int): IO[User] = ???
この場合もアドホックな修正は同じで、map
中でmap
することでそこそこ扱える形になる:
val fxs = xs.map(id => findUserById(id).map(id -> _)) // => List(IO(1 -> User(...)), IO(2 -> User(...)), ...)
しかしIO
のリストは扱いづらいのでsequence
を使ってリストのIO
に変換するのが定石だ。
fxs.sequence // => IO(List(...))
ちなみにmap
してsequence
するのはめちゃくちゃよくあるイディオムなので、traverse
とだけ書けばよいようになっている:
val fxs = xs.traverse(id => findUserById(id).map(id -> _))
Stateモナド
さて、冒頭で「ペアは様々な用途で利用されている」と書いた。面白いことに、関数型プログラミングでおなじみのモナドにもペアは頻出する。
そのうちStateモナドは面白い特性を持っているのでこれから見ていこう。
Stateモナドは、state => (nextState, result)
の形をとる関数、すなわち「状態を受け取り、処理結果と次の状態をペアにして返す」関数を、モナド則を満たすように構成したものである。
モナド則を満たすように構成する、つまりモナドにすると何が嬉しいかというと、逐次合成できるようになって嬉しい。
import cats.implicits._ import cats.data.State val f = State { (n: Int) => (n + 1, n * 2) } // 関数をStateモナドにする // Stateモナドはrunすることで状態遷移させることができる // fに1を渡すと状態は2になり、結果は2になる f.run(1).value // => (2, 2) // fを3つつなげてみる val fff = for { _ <- f _ <- f fff <- f } yield fff // fffに1を渡すと状態は4(初期状態1 + 1 + 1 + 1)になり、結果は6(直前の状態3 * 2)になる fff.run(1).value // => (4, 6)
通常の関数をStateに持ち上げる
なにか別のものを、より便利な特定の型クラスにすることを持ち上げ(lift)と呼ぶ。State
には、普通の関数をState
に持ち上げるためのメソッドがいくつか用意されている:
pure
- 状態を無視して固定値を返すような
State
を作れる。 val s: State[Int, Int] = 42.pure
と書くと、状態を無視してただ42を返す(s.run(1000).value === (1000, 42)
になる)。
- 状態を無視して固定値を返すような
inspect
- 状態を受け取り、何かを処理するが、状態を変更しないような
State
を作れる。 val s: State[Int, Int] = State.inspect(_ * 2)
と書くと、s.run(1000).value === (1000, 2000)
になる。
- 状態を受け取り、何かを処理するが、状態を変更しないような
modify
- 状態を受け取り、ただ状態を変更する。結果は
Unit
になる。 val s: State[Int, Unit] = State.modify[Int](_ * 2)
と書くと、s.run(1000).value === (2000, ())
になる。
- 状態を受け取り、ただ状態を変更する。結果は
apply
- 状態を受け取り、次の状態と結果を返す。
ここで注目したいのがinspect
で、渡す関数はstate => result
の形になっていればよい。そういえば、前の節でmap
に渡していたのもこの形じゃなかったっけ?
Stateでmapする
ご賢察の通り、リストの各要素に対してState
を実行することができる。State
自体はただのデータ構造なので、map
可能な形にするにはrun
を使えばよい。
val xs = List(1,2,3,4,5) val double = (n: Int) => n * 2 xs.traverse(State.inspect(double).run).value // => List((1, 2), (2, 4), (3, 6), (4, 8), (5, 10))
ここではmap
のかわりにtraverse
しているが、これはrun
がいったんEval
というデータ構造を返すためだ。List[Eval]
は使いにくいので、traverse
によってEval[List]
を取り出し、最後に.value
で中身を取り出している。
State.inspect
を使うことで、引数と返り値のペアの形で結果を得ることができた。これは便利だ。
StateT登場!!
といっても、今のところ普通に手でmap
したほうが手っ取り早いのである。
xs.map(x => x -> double(x)) // => List((1, 2), (2, 4), (3, 6), (4, 8), (5, 10))
このままではモナドがオワコンになってしまう。
しかしStateが威力を発揮するのは、他のモナドと組み合わせる必要が出てきたときである。以前の節で例示した、メソッドがOption
を返しうるパターンだとどうか。
// DBからユーザ情報を取得するメソッド def findUserById(id: Int): Option[User] = ??? val xs = List(1,2,3,4,5) val fxs = xs.map(id => findUserById(id).map(id -> _)) // => List(Some(1 -> User(...)), None, ...)
この例ではまだコードのサイズが肥大化せずに済んでいるが、産業コードではコードはどんどん大きくなって処理を追い掛けるのは大変になってしまう。
そこで、StateT
を利用してみよう。
【モナドトランスフォーマー】
他のモナドと合体させて機能を追加したモナドを作れるような機構がある。これをモナドトランスフォーマーと呼ぶ。
モナドトランスフォーマーは、<元となったモナド>Tという名前をとるのが通例だ。
StateT.inspectF
を使って元の関数をラップしよう。inspectF
はinspect
と違って、返り値がなんらかのモナドになっていることを想定しているぞ。
State.inspect
が取る関数:S => A
StateT.inspectF
が取る関数:S => F[A]
例えば「偶数のときにだけ値を2倍するmaybeDouble
」の場合、コードは以下のようになる:
import cats.data.StateT val xs = List(1,2,3,4,5) val maybeDouble = (n: Int) => n match { case nn if n % 2 == 0 => Some(n * 2) case nn => None } xs.map(StateT.inspectF(maybeDouble).run) // => List(None, Some((2,4)), None, Some((4,8)), None)
IO
を利用する場合も、StateT
はうまくいく:
import cats.effect.IO val heavyDouble = (n: Int) => IO { n * 2 } xs.traverse(StateT.inspectF(heavyDouble).run) // => IO(List((1,2), (2,4), (3,6), (4,8), (5,10)))
一般化する
ここまでは手でState
を作っていたが、拡張メソッドを使って綺麗にすることができる。
extension [A, F[_] : cats.Traverse, B](xs: F[A]) def tapMap(f: A => B): F[(A, B)] = xs.traverse(State.inspect(f).run).value extension [A, F[_] : cats.Functor, B](xs: F[A]) def tapMapF[G[_] : cats.Monad](f: A => G[B]): F[G[(A, B)]] = xs.map(StateT.inspectF(f).run) xs.tapMap(double) // => List((1, 2), (2, 4), (3, 6), (4, 8), (5, 10)) xs.tapMapF(maybeDouble) // => List(None, Some((2,4)), None, Some((4,8)), None)
この一般化の嬉しいところは、List
でなくても動作するところだ:
val ys: Either[String, Int] = Right(42) ys.tapMapF(n => (n * 2).some) // => Right(Some((42, 84)))
まとめ
State
はペア構造と密接に結び付いていることを確認した。State
の機能を使うことで、引数を再利用する処理をうまく書けることを確認した。StateT
の機能を使うことで、再利用結果がモナドでも柔軟に処理できることを示した。