何の記事か
Scala Design Patterns で例として挙げられていた Optionモナドを、 cats.Monad を用いて書き換えてみます。以前 Scala Design Patterns のモナドに関する章を噛み砕いで追いました。
では cats の Monad を使うとどのように書けるのだろうか?と思い、色々調べながら実験した内容をメモしました。
cats.Monad
cats.Monad の定義は以下の通りです。
trait Monad[F[_]] extends FlatMap[F] with Applicative[F]
FlatMap[F]
は文字通りflatMap
を持つものApplicative[F]
はap
とpure
を持ち、Functor[F]
を継承している。pure
は以前のブログでまとめたところのunit
に相当する部分だと理解Functor[F]
はmap
を提供していることを以前のブログでもまとめている
これを使って Scala Design Patterns にあるように Option モナドを書き換えてみようと思います。
まずはcats公式のサンプルを見てみます。すると、すでに Applicative[Option]
が存在する場合の例が載っていました。
If Applicative is already present and flatten is well-behaved, extending the Applicative to a Monad is trivial.
import cats._ implicit def optionMonad(implicit app: Applicative[Option]) = new Monad[Option] { // Define flatMap using Option's flatten method override def flatMap[A, B](fa: Option[A])(f: A => Option[B]): Option[B] = app.map(fa)(f).flatten // Reuse this definition from Applicative. override def pure[A](a: A): Option[A] = app.pure(a) @annotation.tailrec def tailRecM[A, B](init: A)(fn: A => Option[Either[A, B]]): Option[B] = fn(init) match { case None => None case Some(Right(b)) => Some(b) case Some(Left(a)) => tailRecM(a)(fn) } }
tailRecM
は初めて見る関数ですが、これは cats の主張として「Monadを再帰的に処理する場面は多々あり、末尾再帰にしないとスタックオーバーフローが発生するので、Monadを定義する際に強制的に末尾再帰を定義させる」とのことです。
In addition to requiring flatMap and pure, Cats has chosen to require tailRecM which encodes stack safe monadic recursion, as described in Stack Safety for Free by Phil Freeman. Because monadic recursion is so common in functional programming but is not stack safe on the JVM, Cats has chosen to require this method of all monad implementations as opposed to just a subset.
例を見ると、catsのMonadは継承するものではなく引数としてimplicitに与えるイメージのようですね。(なぜ継承ではなく移譲になっているのかは、もう少し理解を深める必要がありそうです。)
書き換えてみる
さて、Applicative
の実装は今回のやりたいことから外れるので、Applicative
を利用しているところを書き換えていこうと思います。
flatMap
は値がSome
だったらその中身に対して受け取った関数を実行すれば良い。pure
は引数を受け取るコンストラクタなので、Some
を返せば良い
以上を踏まえると、今回作る MyOption
は次のように定義することができます。
implicit def optionMonad: Monad[MyOption] = new Monad[MyOption] { override def pure[A](x: A): MyOption[A] = MySome(x) override def flatMap[A, B](fa: MyOption[A])(f: A => MyOption[B]): MyOption[B] = fa match { case MySome(value) => f(value) case MyNone() => MyNone() } @tailrec override def tailRecM[A, B](a: A)(f: A => MyOption[Either[A, B]]): MyOption[B] = f(a) match { case MySome(Right(value)) => MySome(value) case MySome(Left(value)) => tailRecM(value)(f) case MyNone() => MyNone() } }
次にこれを実際にどのようにして MyOption
から使うのかですが、自分は
MyOption
のコンパニオンオブジェクトに先ほどのoptionMonad
を implicit に定義し、MyOption
のコンストラクタに implicit で渡す
ようにしてみました。
object MyOption { implicit def optionMonad: Monad[MyOption] = ... } sealed class MyOption[A](implicit val optionMonad: Monad[MyOption]) { def flatMap[B](f: A => MyOption[B]): MyOption[B] = optionMonad.flatMap(this)(f) def map[B](f: A => B): MyOption[B] = optionMonad.map(this)(f) } case class MySome[A](value: A) extends MyOption[A] case class MyNone[A]() extends MyOption[A]
extends MyOption[A]
で MyOption[A]
のコンストラクタ引数が足りないので、Scalaのコンパイラは implicit に定義されたものを探しに行きます。この時に、コンパニオンオブジェクトの中に含まれている implicit 定義もコンパイラの探索対象となります。
変換のソース型か期待されるターゲット型のコンパニオンオブジェクトの中に含まれているimplicit定義も、コンパイラーの探索対象に入る
Scalaスケーラブルプログラミング 第4版, p.415 より引用
IntelliJの View: Show Implicit Hints
を on にすると、暗黙的に渡されているパラメータを表示することができるのですが、それを使うと、きちんと引数に渡されていることが確認できます。
まとめ
使ってあげるとこんな感じになり、map/flatMap が実装されているので for 式でも書くことができます。
object Main { def main(args: Array[String]): Unit = { val result = for { x <- MySome(1) y <- MySome(2) } yield { x + y } println(result) // MyOption(3) } }
今回作成したMyOption
のコードをまとめると以下のようになります。
(Scala with cats や fpinscala も進めていきたいなあ...)