🥰

モナドポエムを書く前にモナドをつくろう

8 min read

tl;dr

関数型(モナド)ポエムを書かない. コードを書きましょう.

モナド(あるいは関数型プログラミングと呼ばれるなにか)を文字通り完全に理解した人が「モナドとは××である」という文章を書きたくなるのはプログラマーの中二病のようなものです. (そしてこの記事もそんな若気の至りのひとつかもしれない...)

インターネッツにあふれる、ぼくがかんがえたさいきょうのもなどのせつめい()を読むまえに
モナドや関数型プログラミングとよばれるプログラミングの基本については
https://github.com/typelevel/catshttps://github.com/scalaz/scalaz 、副作用を扱うエフェクトシステムについては https://github.com/typelevel/cats-effect、 https://github.com/monix/monix
https://github.com/zio/zio のコード、Issue でのディスカッションを読みましょう. ポエムを書く暇があったらコードを書いて OSS にコントリビュートしましょう(^ω^)

monad をつくろう

モナドはモナド則を満たす something です. 何か問題でも? 関数型プログラミングとは関係がありません.(Haskell, PureScriptやScalaにはあるが Elm にモナドはないはず.)
モナドは単なる自己関手の圏におけるモノイド対象だよ. なにか問題でも?

さて、モナドについてあれこれ書く前にモナドを作りましょう.

つよいひと曰く「モナドは象」らしいので Elephant と名付けます.

import cats.Monad

case class Elephant[T](value:T)


implicit val instanceForElephant :Monad[Elephant] = new Monad[Elephant] {
  override def pure[A](x: A): Elephant[A] = Elephant(x)
  override def flatMap[A, B](fa: Elephant[A])(f: A => Elephant[B]): Elephant[B] = fa match {
    case Elephant(value) => f(value)
  }
  override def tailRecM[A, B](a: A)(f: A => Elephant[Either[A,B]]): Elephant[B] =  f(a) match {
    case Elephant(Left(a)) => tailRecM(a)(f)
    case Elephant(Right(b)) => Elephant.apply(b) 
  }
}

はい. モナド(正確には末尾再帰制約付きモナド)が作れました. これで君も関数型プログラマーだ(^ω^)


制作・著作
━━━━━
 ⓃⒽⓀ


さて、これで Elephant はモナドの機能を手に入れました.

ちょっとまて、これの何がうれしいの?とお考えでしょうか.

次のように F[G[T]]G[F[T]] に裏返したり、for 文で手続き的に書いたりできます. モナドになるとこういった便利な関数が使えてうれしいですね(^ω^)

またElephantはモナドトランスフォーマーを使って他のモナドと合成することができます. 合成できてうれしいですね(^ω^) 当然のことながらこの Elephant は何の機能も持たないので合成できたとしても何も凄いことはおこりません😢

def convert[T](sft:Seq[Elephant[T]]):Elephant[Seq[T]] = t.traverse(identity) 

val fcomprehension = for {
  a <- Elephant(1)
  b <- Elephant(2)
} yield (a,b)
 
 val s = StateT.pure[Elephant,Int,Int](1) 
 val w = val k = WriterT.pure[Elephant]

Q: ほかには?
A: いろいろ便利な関数がはえるよ(^ω^)


制作・著作
━━━━━
 ⓃⒽⓀ


直感的には map は値の変換を、flatMap は ある処理をしたあとに次の処理をする 継続 をあらわします.

Elephantがモナドであることを証明しましょう. モナド則のチェックには discipline というライブラリを使います. 以下のように Eq のインスタンスを与えてやれば Elephant がモナドとして扱えることを示せます.

import cats.derived.MkEq
import org.typelevel.discipline._

implicit def eqForElephant[T:Eq] :Eq[Elephant[T]] = MkEq[Elephant[T]]
implicit def arbForElephant[T](implicit a: Arbitrary[T]): Arbitrary[Elephant[T]] = Arbitrary(
  org.scalacheck.Arbitrary.arbitrary[T].map(Elephant.apply)
  
)

val f = FunctorTests[Elephant].functor[Int,Double,String].all.check()
val m = MonadTests[Elephant].monad[Int,String,Double].all.check()

// + monad.ap consistent with product + map: OK, passed 100 tests.
// + monad.applicative homomorphism: OK, passed 100 tests.
// + monad.applicative identity: OK, passed 100 tests.
// + monad.applicative interchange: OK, passed 100 tests.
// + monad.applicative map: OK, passed 100 tests.
// + monad.applicative unit: OK, passed 100 tests.
// + monad.apply composition: OK, passed 100 tests.
// + monad.covariant composition: OK, passed 100 tests.
// + monad.covariant identity: OK, passed 100 tests.
// + monad.flatMap associativity: OK, passed 100 tests.
// + monad.flatMap consistent apply: OK, passed 100 tests.
// + monad.flatMap from tailRecM consistency: OK, passed 100 tests.
// + monad.invariant composition: OK, passed 100 tests.

このように、モナドはモナド則を満たす something です. 何か問題でも

モナドかどうか自動でチェックできてうれしいですね.


さて、ここで時々話題に上がる「Futureはモナド」問題を考えてみましょう.

もっと言うと、「様々な計算の一部の側面を切り取った」ときに、計算合成出来たほうが汎用性高まって嬉しいから モナドに近づけたみたいなものもありそうですよね
非同期計算(Future)なんかはScalaはモナドで、JS(Promise)はモナドじゃないとか; ABAB↑↓BA (@ababupdownba)

「非同期計算(Future)なんかはScalaはモナドで、JS(Promise)はモナドじゃない」🤔

検証してみましょう. モナドは discipline ライブラリを使えばチェックできます.

おやっ?🤔

import scala.concurrent.Future
import cats.laws.discipline.MonadTests
import cats.syntax._

val futureTest = MonadTests[Future].monad[Int,Int,Int].all.check()
// could not find implicit value for parameter EqFA: cats.kernel.Eq[scala.concurrent.Future[Int]]
// コンパイルエラー. Eqが見つかりません😖

おやっ?🤔

implicit def eqForElephant[T:Eq] :Eq[Future[T]] = MkEq[Future[T]]
// Could not derive an instance of Eq[A] where A = scala.concurrent.Future[T].
// コンパイルエラー. derive できないよ 😖

さて、ここで Future の等価性について考えてみましょう.

val f:Future[Int] = Future{Thread.sleep(1.second.toMillis);1}

f は 1秒後までは Future<not complete> の値をとります. 1秒後からは Future(Success(1)) になります. また、今回はあり得ないですが,他の場合では例外が発生して Future(Failure(ex)) をとる可能性もあります. 生成直後の f と 1秒後の f は異なる内部状態をもちます.

Future が モナドかどうかはつまるところ FutureEq の定義をどう与えるかによります. 例えば次のように Future がどんな例外からも復帰して Left になるようにしたうえで Future をブロックしてえられた値を比較するように Eq を定義すれば MonadTest を通すことができます.

この定義ですべての Future がモナドになるとはいえないですね🙄 これはもちろん Promisemap, flatMap, pure と同等のメソッドを備えていてもモナドとは言えないことと同じです.

implicit val throwableEq: Eq[Throwable] =
    Eq.by[Throwable, String](_.toString)
def futureEither[A](f: Future[A]): Future[Either[Throwable, A]] =
    f.map(Either.right[Throwable, A]).recover { case t => Either.left(t) }
implicit def eqfa[A: Eq]: Eq[Future[A]] =
    new Eq[Future[A]] {
      def eqv(fx: Future[A], fy: Future[A]): Boolean = {
        val fz = futureEither(fx).zip(futureEither(fy))
        Await.result(fz.map { case (tx, ty) => tx === ty }, timeout)
      }
    }

val futureTest = MonadTests[Future].monad[Int,Int,Int].all.check()

// + monad.ap consistent with product + map: OK, passed 100 tests.
// + monad.applicative homomorphism: OK, passed 100 tests.
// + monad.applicative identity: OK, passed 100 tests.
// + monad.applicative interchange: OK, passed 100 tests.
// + monad.applicative map: OK, passed 100 tests.
// + monad.applicative unit: OK, passed 100 tests.
// + monad.apply composition: OK, passed 100 tests.
// + monad.covariant composition: OK, passed 100 tests.
// + monad.covariant identity: OK, passed 100 tests.
// + monad.flatMap associativity: OK, passed 100 tests.
// + monad.flatMap consistent apply: OK, passed 100 tests.
// + monad.flatMap from tailRecM consistency: OK, passed 100 tests.
// + monad.invariant composition: OK, passed 100 tests.

非同期計算(Future)なんかはScalaはモナドで、JS(Promise)はモナドじゃない

(´・ω・`)

関数型プログラミングと呼ばれる分野のお話には、わかりやすさのために厳密さが欠けていたり、「ただしほにゃららの条件下で」といった文句が抜けていたりすることが多々あるの
で眉に唾を付けて読みましょう. (もちろんこの文章についても同様に...)

「モナド」や「関数型プログラミング」というたいそうな名前がついていますが、普通にプログラムを書く限りこわいこわい数学や圏論の知識を要求されることはありません. さらにいえばモナド(あるいは「関数型プログラミング」)を理解したからと言ってなにかしら素晴らしい知的ブレークスルーが起こるわけでもありません. もちろん数学の知識があると様々な概念を統一的に扱えてうれしいですがそれはまた別の話. 食わず嫌いせずにまずは cats & cats-effect をインストールしてあれこれいじってみましょう.

https://typelevel.org/cats/

https://typelevel.org/cats-effect/docs/getting-started

※ cats-effect のほうが実際のユースケースがわかりやすくておすすめです.

https://scastie.scala-lang.org/EnEVsbe0SH2p6BCoydAu3Q

ローカルで試すなら↓

build.sbt
libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-core" % "2.6.1",
  "org.typelevel" %% "cats-collection-core" % "0.9.3",
  "org.typelevel" %% "cats-effect" % "3.3.0"
)

補足

モナドや関数型プログラミングと呼ばれる分野の個々の要素だけをみるとあまりありがたみがわかりませんが、さまざまなライブラリのインターフェースが圏論や数学的な知識で共通化されていることで使い勝手が良くなるメリットがあります.

たとえば、 cats を中心にしたエコシステムは基本的に cats-effect というエフェクトシステムと相性が良くなるように作られていますが、型パラメータF[_]をうまく使うことで Monix や ZIO との互換性を与えています. Fが型クラス(例えば Functor, Monad, MonadError, Applicative など)の制約を満たしていればどんなFでもそこに入れられるように設計されています.

ちなみに cats-effect は Scala の Future の問題点を解消した高速なfiber(軽量スレッド) ベースの非同期ランタイムと非同期処理向けのプリミティブ型を提供する実用的なライブラリです. cats ほどアカデミックな雰囲気は強くありません.

逆にアカデミックな色の強い概念もあります. 例えば Recursion Scheme とよばれる手法(matryoshka,droste というライブラリがある)では、Functor という制約を与えることでさまざまな再帰的なデータ構造に対応できる関数を定義することができます.
また、関数型プログラミングと呼ばれる分野の方々はなるべくコンパイラに仕事をさせるライブラリ設計を好んでいて、さまざまな処理・副作用が型によって表現されているライブラリが多く、コンパイル時にエラーをしっかりとチェックすることができます.

これらのありがたみは実際にライブラリを使ってはじめてわかることも多いので、まずは(関数型ポエムではなく) https://github.com/typelevelhttps://github.com/zio のエコシステムが提供するライブラリのドキュメントにざっと目を通すことをおすすめします.

Discussion

ログインするとコメントできます