Grokking Monads in Scala

23
Grokking Monads in Scala St. Louis Lambda Lounge August 5, 2010 Tim Dalton Senior Software Engineer Object Computing Inc.

description

Presentation slide from St. Louis Lambda Lounge presentation on August 5th 2010.

Transcript of Grokking Monads in Scala

Page 1: Grokking Monads in Scala

Grokking Monads in Scala

St. Louis Lambda LoungeAugust 5, 2010

Tim DaltonSenior Software Engineer

Object Computing Inc.

Page 2: Grokking Monads in Scala

Monads Are…

Just a monoid in the category of endofunctors.

Like “duh”!

Page 3: Grokking Monads in Scala

Monads Are…

• A way to structure computations

• A strategy for combining computations into more complex computations

(credit: Julien Wetterwald)

Page 4: Grokking Monads in Scala

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)

Page 5: Grokking Monads in Scala

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)

Page 6: Grokking Monads in Scala

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)

Page 7: Grokking Monads in Scala

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)

Page 8: Grokking Monads in Scala

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.

Page 9: Grokking 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)

}

Page 10: Grokking Monads in Scala

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

Page 11: Grokking Monads in Scala

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

Page 12: Grokking Monads in Scala

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

Page 13: Grokking Monads 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)

Page 14: Grokking Monads in Scala

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 !!!

Page 15: Grokking Monads in Scala

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))

}

}

Page 16: Grokking Monads in Scala

AST Evaluator - Option

println(eval(Divide(Divide(Constant(1972),Constant(2)), Constant(23))))

Some(42)

println(eval(Divide(Constant(1),Constant(0))))

None

Page 17: Grokking Monads in Scala

“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)

})

}

Page 18: Grokking Monads in Scala

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

Page 19: Grokking Monads in Scala

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

Page 20: Grokking Monads in Scala

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)

Page 21: Grokking Monads in Scala

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.

Page 22: Grokking Monads in Scala

Discussion

Can monads ever be “mainstream” ?

Page 23: Grokking Monads in Scala

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/