Download - Grokking Monads in Scala
Grokking Monads in Scala
St. Louis Lambda LoungeAugust 5, 2010
Tim DaltonSenior Software Engineer
Object Computing Inc.
Monads Are…
Just a monoid in the category of endofunctors.
Like “duh”!
Monads Are…
• A way to structure computations
• A strategy for combining computations into more complex computations
(credit: Julien Wetterwald)
Monads Are…• In Haskell, two core functions
(>>=) :: m a -> (a -> m b) -> m breturn :: a -> m a
• Monad Laws
• Left Unit: (return a) >>= k = k a
• Right Unitm >>= (return) = m
• Associative m >>= (\a -> (k a) >>= (\b -> h b)) =
(m >>= (\a -> k a)) >>= (\b -> h b)
Haskell Monads• Haskell supports “do notation” for chaining monadic
computations:
do { x <- Just (3+5) y <- Just (5*7) return (x-y)}
• Which is “sugar” for:
Just (3+5) >>= \x -> Just (5*7) >>= \y -> return (x-y)
• Should evaluate to: Just(-27)
Scala "For comprehensions"for (i <- 1 to 5) yield iscala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5)
for (i <- 1 to 5 if i % 2 == 0) yield iscala.collection.immutable.IndexedSeq[Int] = Vector(2, 4)
for (i <-1 to 5 if i % 2 == 0) { print (i + " " ) }2 4
for (i <-1 to 5 if i % 2 == 0; j <- 1 to 5 if j % 2 != 0) yield ( i * j )scala.collection.immutable.IndexedSeq[Int] = Vector(2, 6, 10, 4, 12, 20)
for (i <-1 to 5 if i % 2 == 0; j <- 1 to 5 if j % 2 != 0; k <- 1 to 5) yield ( i * j / k )scala.collection.immutable.IndexedSeq[Int] = Vector(2, 1, 0, 0, 0, 6, 3, 2, 1, 1, 10, 5, 3, 2, 2, 4, 2, 1, 1, 0, 12, 6, 4, 3, 2, 20, 10, 6, 5, 4)
De-sugarized For comprehensions(1 to 5).map(identity)scala.collection.immutable.IndexedSeq[Int] = Vector(1, 2, 3, 4, 5)
(1 to 5).filter{_ % 2 == 0}.map(identity)scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4)
(1 to 5).filter{_ % 2 == 0}.foreach { i => print (i + " " ) }2 4
(1 to 5).filter{_ % 2 == 0}.flatMap { i => (1 to 5).filter{_ % 2 != 0}.map{ j => i * j } }scala.collection.immutable.IndexedSeq[Int] = Vector(2, 6, 10, 4, 12, 20)
(1 to 5).filter{_ % 2 == 0}.flatMap { i => (1 to 5).filter{_ % 2 != 0}.flatMap{ j => (1 to 5).map{ k => i * j / k } }}scala.collection.immutable.IndexedSeq[Int] = Vector(2, 1, 0, 0, 0, 6, 3, 2, 1, 1, 10, 5, 3, 2, 2, 4, 2, 1, 1, 0, 12, 6, 4, 3, 2, 20, 10, 6, 5, 4)
A Monadic Traitabstract trait M[A] { def unit[B] (value : B):M[B] def map[B](f: A => B) : M[B] = flatMap {x => unit(f(x))} def flatMap[B](f: A => M[B]) : M[B]}
• Scala flatMap correlates to Haskell’s bind (>>=)
• Scala map can be expressed in terms of flatMap or vice versa
• Some implementations use map and flatten
• Haskell convention for flatten is “join”
• Trait is used for illustration. There are many ways to implement monads in Scala.
Simplest Monad – Identity
case class Identity[A](value:A) {
def map[B](f:(A) => B) = Identity(f(value))
def flatMap[B](f:(A) => Identity[B]) = f(value)
}
AST Evaluator• An evaluator for an Abstract Syntax Tree (AST) is going to implemented for illustration
• Scala Trait for evaluator:
trait EvaluatorTrait[A,M] {
def eval(a:A):M;
}
• M will be a Monadic type
• Example used derives from Phillip Wadler’s “Monads for functional programming” paper.
• Can only handle integer constants and division operations
sealed abstract class Term()
case class Constant(value:Int) extends Term
case class Divide(a:Term, b:Term) extends Term
AST Evaluator – Identityobject IdentityEvaluator extends
EvaluatorTrait[Term, Identity[Int]]
{
def eval(term: Term) = term match {
case Constant(x) => Identity(x)
case Divide(a,b) => for (bp <- eval(b);
ap <- eval(a)) yield (ap/bp)
}
println(eval(Divide(Divide(Constant(1972),Constant(2)), Constant(23))))
Identity(42)
println(eval(Divide(Constant(1),Constant(0))))
Exception in thread "main" java.lang.ArithmeticException: / by zero
Useful Monad - Optionsealed abstract class Option[+A] extends Product {
def map[B](f: A => B): Option[B] =
if (isEmpty) None else Some(f(this.get))
def flatMap[B](f: A => Option[B]): Option[B] =
if (isEmpty) None else f(this.get)
}
final case class Some[+A](x: A) extends Option[A] {
def isEmpty = false
def get = x
}
case object None extends Option[Nothing] {
def isEmpty = true
def get = throw new NoSuchElementException("None.get")
}
• Also referred to as the Maybe or Failure monad.
• Haskell supports Just/Nothing that correlates to Some/None in Scala
Usefulness of Optionval areaCodes = Map(
"Fenton" -> 636,
"Florissant" -> 314,
"Columbia" -> 573 )
val homeTowns = Map(
"Moe" -> "Columbia",
"Larry" -> "Fenton",
"Curly" -> "Florissant",
"Schemp" -> "St. Charles” )
def personAreaCode(person:String) =
for (homeTown <- homeTowns.get(person);
areaCode <- areaCodes.get(homeTown)) yield (areaCode)
Usefulness of Optionprintln(personAreaCode("Moe"))
Some(573)
println(personAreaCode("Schemp"))
None
println(personAreaCode("Joe"))
None
println(
for (areaCode <- areaCodes if areaCode._2 == 314;
stoogeHome <- homeTowns if stoogeHome._2 == areaCode._1)
yield stoogeHome._1
)
List(Curly)
Look Mom, No null checks !!!
AST Evaluator - Option
object OptionDivide
extends ((Option[Int], Option[Int]) => Option[Int]) {
def apply(a:Option[Int], b:Option[Int]) =
for (bp <- b;
ap <- if (bp != 0) a else None) yield (ap/bp)
}
object OptionEvaluator extends EvaluatorTrait[Term, Option[Int]] {
def eval(term: Term) = term match {
case Constant(x) => Some(x)
case Divide(a,b) => OptionDivide(eval(a), eval(b))
}
}
AST Evaluator - Option
println(eval(Divide(Divide(Constant(1972),Constant(2)), Constant(23))))
Some(42)
println(eval(Divide(Constant(1),Constant(0))))
None
“Wonkier” Monad – Stateobject State {
def unit[S,A](a:A) = new State((s:S) => (s, a))
}
case class State[S, A](val s:S => (S, A)) {
def map[B](f: A => B): State[S,B] =
flatMap((a:A) => State.unit(f(a)))
def flatMap[B](f: A => State[S,B]): State[S,B] =
State((x:S) => {
val (a,y) = s(x)
f(y).s(a)
})
}
State Monadval add = (x:Int, y:Int) =>
State[List[String], Int]((s:List[String]) => {
((x + " + " + y + " = " + (x + y)) :: s, (x + y))
})
val sub = (x:Int, y:Int) =>
State[List[String], Int]((s:List[String]) => {
((x + " - " + y + " = " + (x - y)) :: s, (x - y))
})
val f = for (x1 <- add(2 , 2); x2 <- sub(x1, 5); x3 <- add(x2, 2)) yield (x3)
val result = f.s(Nil)
println("log = " + result._1.reverse)
log = List(2 + 2 = 4, 4 - 5 = -1, -1 + 2 = 1)
println("result = " + result._2)
result = 1
State Monad – No Sugar
val f = add(2,2).flatMap{ x1 =>
sub(x1, 5).flatMap { x2 =>
add(x2,2)
}
}.map(identity)
val result = f.s(Nil)
println("log = " + result._1.reverse)
log = List(2 + 2 = 4, 4 - 5 = -1, -1 + 2 = 1)
println("result = " + result._2)
result = 1
AST Evaluator - Stateobject StateEvaluator
extends EvaluatorTrait[Term, State[Int, Option[Int]]]
{
def eval(term: Term) = term match {
case Constant(x) => State((s:Int) => (s + x, Some(x))) case Divide(a,b) => for (
evala <- eval(a);
evalb <- eval(b)) yield OptionDivide(evala, evalb)
}
println(eval(Divide(Divide(Constant(1972),Constant(2)), Constant(23))).s(0))
(1997,Some(42))
println(eval(Divide(Constant(20),Constant(0))).s(0))
(20,None)
Summary• Scala supports monadic style of computation to emulate features of “purer” functional programming languages
• For comprehensions imitate the functionality Haskell “do notation”
• Monadic computations can hide a lot of implementation details from those using them.
• Failures using Option• State such as logging using the State monad.
Discussion
Can monads ever be “mainstream” ?
Links
James Iry – “Monads are Elephants”
http://james-iry.blogspot.com/2007/09/monads-are-elephants-part-1.html
http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-2.html
http://james-iry.blogspot.com/2007/10/monads-are-elephants-part-3.html
Philip Wadler’s Monad Papers
http://homepages.inf.ed.ac.uk/wadler/topics/monads.html
Brian Beckman Monad Videos
http://channel9.msdn.com/shows/Going+Deep/Brian-Beckman-Dont-fear-the-Monads/
http://channel9.msdn.com/shows/Going+Deep/Brian-Beckman-The-Zen-of-Expressing-State-The-State-Monad/