ひらめの日常

日常のメモをつらつらと

【Scala】Optionモナドをcats.Monadを使って書いてみる

何の記事か

Scala Design Patterns で例として挙げられていた Optionモナドを、 cats.Monad を用いて書き換えてみます。以前 Scala Design Patterns のモナドに関する章を噛み砕いで追いました。

hiramekun.hatenablog.com

では cats の Monad を使うとどのように書けるのだろうか?と思い、色々調べながら実験した内容をメモしました。

cats.Monad

cats.Monad の定義は以下の通りです。

trait Monad[F[_]] extends FlatMap[F] with Applicative[F]
  • FlatMap[F] は文字通り flatMap を持つもの
  • Applicative[F]appure を持ち、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 より引用

IntelliJView: 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 も進めていきたいなあ...)

参考