- モナドとは何か
- モナドの基本的な扱い方
- モナド変換子の使い方
(defprofile
大橋 賢人 [OHASHI Kent]
:company 株式会社オプト テクノロジー開発2部
:github @lagenorhynque
:twitter @lagenorhynque
:languages [Python Haskell Clojure Scala English français Deutsch русский]
:interests [プログラミング 語学 数学])
一言で言えば、 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に相当する機能を備えている。
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)
F[A] => F[A => B] => F[B]
scalaz.Apply.ap
, scalaz.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)
F[A] => (A => F[B]) => F[B]
scalaz.Bind.bind
, scalaz.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
を単独で使う場合
構造に注目して分解(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
}
高階関数 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"
}
}
モナドのためのシンタックスシュガーを活用する。
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]
として扱う必要がある場合
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)
}
- 複数階層のモナドの分解・再構築が煩わしい
- 構造に強く依存しているため変更に弱い
- パターンマッチがネストして書きづらく読みづらい
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"
}
}
}
- 構造を直接扱う必要はないが関数がネストして書きづらく読みづらい
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
式が連鎖して書きづらく読みづらい
一言で言えば、あるモナドに別のモナドを上乗せ(合成)したモナド。
ネストしたモナドをネストしていないかのように扱えるようになる。
e.g. scalaz.OptionT
, scalaz.EitherT
, scalaz.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]