mp in scala
TRANSCRIPT
MP in Scala
MP = Monadic Programming
1. モナドとは何か2. モナドの基本的な扱い方3. モナド変換子の使い方
自己紹介
(defprofile 大橋 賢人 [OHASHI Kent] :company 株式会社オプト テクノロジー開発2部 :github @lagenorhynque :twitter @lagenorhynque :languages [Python Haskell Clojure Scala English français Deutsch русский] :interests [プログラミング 語学 数学])
モナド(Monad)とは
一言で言えば、 flatMap できる型クラス(type class)。
F[A] => (A => F[B]) => F[B]
型クラスFunctor, Applicativeの上位互換。
// scalaz.Monad trait Monad[F[_]] extends Applicative[F] with Bind[F] { abstract def bind[A, B](fa: F[A])(f: (A) ⇒ F[B]): F[B] abstract def point[A](a: ⇒ A): F[A] ... }
-- Haskell class Applicative m => Monad m where (>>=) :: forall a b. m a -> (a -> m b) -> m b return :: a -> m a ...
モナドの具体例
例えば、 。Option
標準ライブラリでは型クラスとして実装されているわけではないが
Functor, Applicative, Monadに相当する機能を備えている。
Functorの map
F[A] => (A => B) => F[B]
にあたる。scalaz.Functor.map
val n: Option[Int] = Some(1) val f: Int => String = x => x.toString
// Option#map n.map(f) // Option#flatMap による実装 n.flatMap { a => Some(f(a)) } // for式による実装 for { a <- n } yield f(a)
Applicativeの ap / <*>
F[A] => F[A => B] => F[B]
, にあたる。scalaz.Apply.apscalaz.syntax.ApplyOps.<*>
val n: Option[Int] = Some(1) val f: Option[Int => String] = Some(x => x.toString)
// Option#flatMap, Option#map による実装 n.flatMap { a => f.map { g => g(a) } } // for式による実装 for { a <- n g <- f } yield g(a)
Monadの flatMap / bind / >>=
F[A] => (A => F[B]) => F[B]
, にあたる。scalaz.Bind.bindscalaz.syntax.BindOps.>>=
val n: Option[Int] = Some(1) val f: Int => Option[String] = x => Some(x.toString)
// Option#flatMap n.flatMap(f) // for式による実装 for { a <- n b <- f(a) } yield b
同種のモナドを扱う場合
例えば、 Option を単独で使う場合
同種のモナドを扱う場合
(1) パターンマッチ
構造に注目して分解(unapply, destructure)する。
case class User(firstName: Option[String], lastName: Option[String])
def userName(user: User): Option[String] = user match { case User(Some(first), Some(last)) => Some(s"$first $last") case _ => None }
同種のモナドを扱う場合
(2) 高階関数
高階関数 map, flatMap, etc.を組み合わせる。
case class User(firstName: Option[String], lastName: Option[String])
def userName(user: User): Option[String] = user.firstName.flatMap { first => user.lastName.map { last => s"$first $last" } }
同種のモナドを扱う場合
(3) for式
モナドのためのシンタックスシュガーを活用する。
case class User(firstName: Option[String], lastName: Option[String])
def userName(user: User): Option[String] = for { first <- user.firstName last <- user.lastName } yield s"$first $last"
異種のモナドが混在する場合
例えば、 Option と を組み合わせてscalaz.\/
E \/ Option[A] として扱う必要がある場合
異種のモナドが混在する場合
(1) パターンマッチ
import scalaz._, Scalaz._
case class User(id: Int, firstName: Option[String], lastName: Option[String]) def userRole(id: Int): Error \/ Role = ???
def userInfo(user: User): Error \/ Option[String] = userRole(user.id) match { case \/-(role) => user match { case User(_, Some(first), Some(last)) => \/-(Some(s"$first $last: $role")) case _ => \/-(None) } case -\/(error) => -\/(error) }
問題点
複数階層のモナドの分解・再構築が煩わしい構造に強く依存しているため変更に弱い
パターンマッチがネストして書きづらく読みづらい
異種のモナドが混在する場合
(2) 高階関数
import scalaz._, Scalaz._
case class User(id: Int, firstName: Option[String], lastName: Option[String]) def userRole(id: Int): Error \/ Role = ???
def userInfo(user: User): Error \/ Option[String] = userRole(user.id).map { role => user.firstName.flatMap { first => user.lastName.map { last => s"$first $last: $role" } } }
問題点
構造を直接扱う必要はないが関数がネストして書きづらく読みづらい
異種のモナドが混在する場合
(3) for式
import scalaz._, Scalaz._
case class User(id: Int, firstName: Option[String], lastName: Option[String]) def userRole(id: Int): Error \/ Role = ???
def userInfo(user: User): Error \/ Option[String] = for { role <- userRole(user.id) } yield for { first <- user.firstName last <- user.lastName } yield s"$first $last: $role"
問題点
関数はネストしないがfor式が連鎖して書きづらく読みづらい
モナド変換子(monad transformer)とは
一言で言えば、あるモナドに別のモナドを上乗せ(合成)したモナド。
ネストしたモナドをネストしていないかのように扱えるようになる。
e.g. , , scalaz.OptionTscalaz.EitherTscalaz.ListT
モナド変換子の生成と変換
// 型パラメータを1個とる型F(モナド) type F[A] = ???
// FとOptionでネストしたモナド val fOptionA: F[Option[A]] = ??? // OptionとFを合成したOptionT val optionTFA: OptionT[F, A] = ??? // Option val optionA: Option[A] = ??? // F val fA: F[A] = ???
// F[Option[A]] → OptionT[F, A] OptionT.optionT(fOptionA) // OptionT[F, A] → F[Option[A]] optionTFA.run // Option[A] → F[Option[A]] → OptionT[F, A] OptionT.optionT(optionA.point[F]) // F[A] → OptionT[F, A] fA.liftM[OptionT]
モナド変換子の導入
ここではモナド変換子 scalaz.OptionT を利用して
Option と scalaz.\/ を合成してみる。
import scalaz._, Scalaz._
case class User(id: Int, firstName: Option[String], lastName: Option[String]) def userRole(id: Int): Error \/ Role = ???
type ErrorOrResult[+A] = Error \/ A
def userInfo(user: User): Error \/ Option[String] = (for { role <- userRole(user.id).liftM[OptionT] first <- OptionT.optionT(user.firstName.point[ErrorOrResult]) last <- OptionT.optionT(user.lastName.point[ErrorOrResult]) } yield s"$first $last: $role").run
Option[+A] と E \/ A をfor式1つでシンプルに扱える
モナド変換子への変換がやや冗長
さらにリファクタ
モナド変換子への変換を関数として抽出してみる。
import scalaz._, Scalaz._
case class User(id: Int, firstName: Option[String], lastName: Option[String]) def userRole(id: Int): Error \/ Role = ???
type ErrorOrResult[+A] = Error \/ A def fromOption[A](a: Option[A]): OptionT[ErrorOrResult, A] = OptionT.optionT(a.point[ErrorOrResult]) def fromEither[A](a: ErrorOrResult[A]): OptionT[ErrorOrResult, A] = a.liftM[OptionT]
def userInfo(user: User): Error \/ Option[String] = (for { role <- userRole(user.id) ▹ fromEither first <- user.firstName ▹ fromOption last <- user.lastName ▹ fromOption } yield s"$first $last: $role").run
ちなみに
Q: このtype aliasは何のためにあるのか?
type ErrorOrResult[+A] = Error \/ A
A: モナド変換子の型パラメータにカインド(kind)を合わせるため。
型 カインド
OptionT[F[_], A]のF[_] * -> *
\/[+A, +B] * -> * -> *
// 方法1: type aliasする type ErrorOrResult[+A] = Error \/ A OptionT[ErrorOrResult, A]
// 方法2: インラインでtype aliasする(type lambda) OptionT[({type λ[+α] = Error \/ α})#λ, A]
// 方法3: コンパイラプラグインKind Projectorを利用する OptionT[Error \/ +?, A]
Further Reading
Functor, Applicative, Monadのシンプルな定式化 - Qiita
ScalaでFutureとEitherを組み合わせたときに綺麗に書く方法 - scalaとか・・・
Scalaz Monad Transformers - Underscore
独習 Scalaz — モナド変換子
Easy Monad
Haskell モナド変換子 超入門 - Qiita
All About Monads - Sampou.Org
Source CodeGist - lagenorhynque - monad-transformers