Monads
Data structures with map and flatMap seem to be quite common. In fact there's a name that describe this class of a data structures together with some algebraic laws that they should have. They are called Monads. A monad M is a parametric type M[T] with two operations, flatMap and unit, that have to satisfy some laws.
- train M[T]{
- def flatMap[U](f: T=> M[U]):M[U]
- }
- def unit[T]:(x:T): M[T]
Example of Monads
flatMap is an operation on each of these types, whereas unit in Scala is different for each monad.
Monads and map
map can be defined for every monad as a combination of flatMap and unit:
- m map f == m flatMap (x => unit(f(x))) == m flatMap(f andThen unit)
Monad Laws
To qualify as a monad, a type has to satisfy three laws:
* Associativity
- m flatMap f flatMap g == m flatMap (x => f(x) flatMap g)
- unit(x) flatMap f == f(x)
- m flatMap unit == m
- val x = 1
- val m = List(1)
- val unit:Int=>List[Int] = x => List(x)
- val f:Int=>List[Int] = x=>List(x+1)
- val g:Int=>List[Int] = x=>List(x*2)
- val list = List(1,2,3,4)
- val a1 = list flatMap f flatMap g
- val a2 = list flatMap {x => f(x) flatMap g}
- printf("Satisfy Associatity Law? %s\n", a1 == a2)
- // unit(x) flatMap f == f(x)
- val l1 = unit(x) flatMap (f)
- printf("Satisfy Left Unit Law? %s\n", l1 == f(x))
- // m flatMap unit == m
- val r1 = m flatMap unit
- printf("Satisfy Right Unit Law? %s\n", r1 == m)
Let's check the monad laws for Option. Here's flatMap for Option:
- abstract class Option[+T]{
- def flatMap[U](f:T=>Option[U]):Option[U] = this match {
- case Some(x) => f(x)
- case None => None
- }
- }
Need to show Some(x) flatMap f == f(x)
- Right Unit Raw
Need to show: opt flatMap Some == opt
- Associative Law
Need to show: opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g)
Let's check in with code review:
- // Checking Monad Raw
- val f2:Int=>Option[Int]=x => Option(x+1)
- val g2:Int=>Option[Int]=x => Option(x*2)
- val map = Map[String,Int]("john"->18, "mary"->20, "ken"->19)
- val opt = map get ("john") // opt is type of Option
- printf("opt with type=%s\n", opt.getClass.getName)
- // Checking Left Unit Raw: unit(x) flatMap f == f(x)
- val lur = opt flatMap f2
- printf("Satisfy Left Unit Law? %s (%s)\n", lur==f2(opt.getOrElse(-1)), lur)
- // Checking Right Unit Raw: opt flatMap Some == opt
- val optUnit:Int=>Option[Int]=x=>Option(x)
- val rur = opt flatMap optUnit
- printf("Satisfy Right Unit Law? %s (%s)\n", rur==opt, rur)
- // Checking the Associative Law: opt flatMap f flatMap g == opt flatMap (x => f(x) flatMap g)
- val asr = opt flatMap f2 flatMap g2
- printf("Satisfy Association Law? %s (%s)\n", asr == (opt flatMap(x => f2(x) flatMap g2)), asr)
We have seen that monad-typed expressions are typically written as for expressions. What is the significance of the laws with respect to this?
1. Associativity says essentially that one can “inline” nested for expressions:
2. Right Unit Raw Says
Intuitively:
3: Left unit does not have an analogue for for-expressions.
Another type: Try
In the later parts of this course we will need a type named Try. Try resembles Option, but instead of Some/None there is a Success case with a value and a Failure case that contains an exception:
- abstract class Try[+T]
- case class Success[T](x:T) extends Try[T]
- case class Failure[T](ex:Throwable) extends Try[Nothing]
Creating a Try
You can wrap up an arbitrary computation in a Try.
- Try(expr) // gives Success(someValue) or Failure(someException)
- object Try {
- def apply[T](expr: => T):Try[T]=
- try Success(expr)
- catch{
- case NonFatal(ex) => Failure(ex)
- }
- }
Just like with Option, Try-valued computations can be composed in for expresssions.
If computeX and computeY succeed with results Success(x) and Success(y), this will return Success(f(x, y)). If either computation fails with an exception ex, this will return Failure(ex).
Definition of flatMap and map on Try
- abstract class Try[+T] {
- def flatMap[U](f: T => Try[U]): Try[U] = this match {
- case Success(x) => try f(x) catch { case NonFatal(ex) => Failure(ex) }
- case Failure(ex) => Failure(ex)
- }
- def map[U](f: T => U): Try[U] = this match {
- case Success(x) => Try(f(x))
- case Failure(ex) => Failure(ex)
- }
- }
- t map f == t flatMap (x => Try(f(x))) == t flatMap (f andThen Try)
- Try(expr) flatMap f != f(expr)
Call this the “bullet-proof” principle.
Conclusion
We have seen that for-expressions are useful not only for collections. Many other types also define map, flatMap, and withFilter operations and with them for-expressions.
Many of the types defining flatMap are monads. (If they also define withFilter, they are called “monads with zero”). The three monad laws give useful guidance in the design of library APIs.
沒有留言:
張貼留言