grokking monads in scala
DESCRIPTION
Presentation slide from St. Louis Lambda Lounge presentation on August 5th 2010.TRANSCRIPT
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/