Playing with the State Monad and Functional Programming
1. Playing with the State Monad
David Galichet
Freelance Developer
Twitter : @dgalichet
2. Wait ! what’s a Monad ?
•
Functor
•
Monad
•
For comprehensions
3. Wait ! what’s a Monad ?
y !
r e
o d
g i
te s
a in
c
o ry
N o
e
th
•
Functor
•
Monad
•
For comprehensions
4. Functor
F is a Functor if there is a function :
map(fa: F[A])(f: A => B): F[B]!
that implements the following laws :
1. Identity : map(fa)(Id) == fa!
2. Composition : map(fa)( f ○ g ) ==
map(map(fa)(f))(g)
5. Functor
F can be seen as a context where a value A rely
F is a Functor if there is a function :
map(fa: F[A])(f: A => B): F[B]!
that implements the following laws :
1. Identity : map(fa)(Id) == fa!
2. Composition : map(fa)( f ○ g ) ==
map(map(fa)(f))(g)
6. Functor
F is a Functor if there are the following functions :
pure[A](a: A): F[A]!
map(fa: F[A])(f: A => B): F[B]!
Id is the Identity function
that implements the following laws
1. Identity : map(Id) == Id!
2. Composition : map( f ○ g ) == fmap(f) ○
fmap(g)
10. Ex: Maybe is a Functor
sealed trait Maybe[+A]!
case class Value[+A](a: A) extends Maybe[A]!
case object Empty extends Maybe[Nothing]!
!
object Maybe {!
implicit val maybeIsAFunctor = new Functor[Maybe] {!
def pure[A](a: A): Maybe[A] = Value(a)!
def map[A, B](fa: Maybe[A])(f: A => B) = fa match {!
case Empty => Empty!
case Value(a) => Value(f(a))!
}!
}!
}
11. Ex: Maybe is a Functor
We define a generic function that double the content of a
Functor :
import monads.MaybeIsAFunctor!
def twice[F[+_]](fa: F[Int])(implicit FA: Functor[F]):
F[Int] = FA.map(fa){ x => x*2 }!
!
scala> twice(Value(4): Maybe[Int])!
res1: Value(8)
12. Monad
M is a Monad if M is an (Applicative) Functor and
there exists the following functions :
unit[A](a: A): M[A]!
bind[A,B](ma: M[A])(f: A => M[B]): M[B]!
13. Monad
and methods unit and bind implement the following
laws :
1. Left Identity : bind(unit(x))(f) == f(x) !
2. Right Identity : bind(ma)(unit) == ma!
3. Associativity :
bind(bind(ma)(f))(g) == bind(ma){ a =>
bind(f(a))(g) }
15. Ex : Maybe is a Monad
implicit val maybeIsAMonad = new Monad[Maybe] {!
def pure[A](a: A) = Value(a)!
!
def map[A, B](fa: Maybe[A])(f: (A) => B): M[B] = ???!
!
def bind[A, B](ma: Maybe[A])(f: A => Maybe[B]): M[B] =
ma match {!
case Empty => Empty!
case Value(a) => f(a)!
}!
}
16. Ex : Maybe is a Monad
implicit val maybeIsAMonad = new Monad[Maybe] {!
def pure[A](a: A) = Value(a)!
!
def map[A, B](fa: Maybe[A])(f: (A) => B) = bind(fa)
{ a => unit(f(a)) }!
!
def bind[A, B](ma: Maybe[A])(f: A => Maybe[B]): M[B] =
ma match {!
case Empty => Empty!
case Value(a) => f(a)!
}!
}
17. Ex : Maybe is a Monad
We define a generic function that add the content of two
Monads :
def add[M[+_]](ma: M[Int], mb: M[Int])(implicit MA:
Monad[M]): M[Int] = !
MA.bind(ma) { x => MA.map(mb) { y => x + y} }!
!
scala> import monads.maybeIsAMonad!
scala> add(Value(4): Maybe[Int], Value(2): Maybe[Int])!
res1: monads.Maybe[Int] = Value(6)
18. For comprehension
•
Scala provides For Comprehension to simplify
chaining of map and flatMap (equivalent to do
notation in Haskell)
•
At compilation, For Comprehension will be
transformed to a serie of flatMap and map
19. For comprehension
Scala For Comprehension needs that map and
flatMap to be defined on object directly (not using
typeclass). Here we define a MonadWrapper :
implicit class MonadWrapper[A, M[+_]](ma: M[A])(implicit
MA: Monad[M]) {!
def map[B](f: A => B): M[B] = MA.map(ma)(f)!
!
def flatMap[B](f: A => M[B]): M[B] = MA.flatMap(ma)(f)!
}
20. For comprehension
import monads.maybeIsAMonad!
!
def add2[M[+_]](ma: M[Int], mb: M[Int])(implicit MA:
Monad[M]): M[Int] = {!
import Monad.MonadWrapper!
for {!
a <- ma!
b <- mb!
} yield a + b!
}!
!
scala> import monads.maybeIsAMonad!
scala> add2(Value(4): Maybe[Int], Value(2): Maybe[Int])!
res2: monads.Maybe[Int] = Value(6)
21. Generic programming
def sequence[A, M[+_]](ms: List[M[A]])(implicit MA:
Monad[M]): M[List[A]] = ms match {!
case Nil => MA.unit(List.empty[A])!
case head::tail => for {!
x <- head!
xs <- sequence(tail)!
} yield x::xs!
}!
!
import monads.maybeIsAMonad!
!
scala> Monad.sequence(List(Value(1): Maybe[Int],
Value(2): Maybe[Int]))!
res3: monads.Maybe[List[Int]] = Value(List(1, 2))
23. Rules of the game
•
We want to simulate two robots moving through a
nxm playground
•
Each robot can either turn on a direction (North,
South, East, West) or move one step forward
•
Robots move or turn according to instructions
24. Rules of the game
•
A robot can’t go out of the playground
•
A robot will be blocked if another robot is on the
place
•
Some coins are spread on the playground
•
Robots gather coins when they move over it
25. Think about this game
•
It appears that we will deal with many states :
•
Playground with its coins
•
Robots with their positions and gathered coins
26. We want functional purity
•
Functional Purity has many advantages like
composability, idempotence, maintainability and
thread safety
•
We need to find a way to deal with states and
remain pure
27. Dealing with states
S => (S, A)
•
S is the type of a state and A the type of a
computation
•
The outcome of this function is a new state and a
result
30. Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = ???!
def flatMap[B](f: A => State[S, B]): State[S, B] = ???!
}!
!
object State {!
def apply[S, A](f: S => (S, A)): State[S, A] = ???!
}
31. Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = ???!
def flatMap[B](f: A => State[S, B]): State[S, B] = ???!
}!
!
object State {!
def apply[S, A](f: S => (S, A)): State[S, A] = !
new State[S, A] {!
def run(initial: S): (S, A) = f(initial)!
}!
State Monad embed computation !
}
32. Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
!
def map[B](f: A => B): State[S, B] = State { s =>!
val (s1, a) = run(s)!
(s1, f(a))!
Don’t forget the definition:
}!
State.apply(S => (S, A)): State[S,A]
!
!
!
def flatMap[B](f: A => State[S, B]): State[S, B] = ???!
}
33. Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
!
def map[B](f: A => B): State[S, B] = State { s =>!
val (s1, a) = run(s)!
(s1, f(a))!
}!
Don’t forget the definition:
!
State.apply(S => (S, A)): State[S,A]
!
!
def flatMap[B](f: A => State[S, B]): State[S, B] = !
State { s =>!
val (s1, a) = run(s)!
f(a).run(s1)!
}!
}
34. Coming back to our game !
•
We drive robots using a list of instructions
sealed trait Instruction!
case object L extends Instruction // turn Left!
case object R extends Instruction // turn Right!
case object A extends Instruction // Go on
35. Coming back to our game !
•
Each robot has a direction
sealed trait Direction {!
def turn(i: Instruction): Direction!
}!
case object North extends Direction {!
def turn(i: Instruction) = i match {!
case L => West!
case R => East!
case _ => this!
}!
}!
case object South extends Direction { ... }!
case object East extends Direction { ... }!
case object West extends Direction { ... }
36. Coming back to our game !
•
A direction and a location define a position
case class Point(x: Int, y: Int)!
!
case class Position(point: Point, dir: Direction) {!
def move(s: Playground): Position = {!
val p1 = dir match {!
case North => copy(point = point.copy(y = point.y + 1))!
case South => ...!
}!
if (s.isPossiblePosition(p1)) p1 else this!
}!
def turn(instruction: Instruction): Position =
!
copy(direction = direction.turn(instruction))!
}
37. Coming back to our game !
•
And each Robot is a player with a Score
sealed trait Player!
case object R1 extends Player!
case object R2 extends Player!
!
case class Score(player: Player, score: Int)
38. Coming back to our game !
•
The state of each Robot is defined as :
case class Robot(!
player: Player, !
positions: List[Position], !
coins: List[Point] = Nil) {!
lazy val currentPosition = positions.head!
!
lazy val score = Score(player, coins.size)!
!
def addPosition(next: Position) = copy(positions =
next::positions)!
!
def addCoin(coin: Point) = copy(coins = coin::coins)!
}
39. Coming back to our game !
•
Robots evolve in a playground :
case class Playground(!
bottomLeft: Point, topRight: Point, !
coins: Set[Point],!
r1: Robot, r2: Robot) {!
!
def isInPlayground(point: Point): Boolean =!
bottomLeft.x <= point.x && ...!
!
def isPossiblePosition(pos: Position): Boolean = ...!
!
lazy val scores = (r1.score, r2.score)!
!
def swapRobots(): Playground = copy(r1 = r2, r2 = r1)!
}
40. Look what we did
•
a set of Instructions,
•
a Position composed with Points and Direction,
•
a definition for Players and Score,
•
a way to define Robot state
•
and a way to define Playground state
41. Let put these all together !
•
Now, we need a method to process a single
instruction
•
And a method to process all instructions
•
The expected result is a State Monad that will be
run with the initial state of the playground
42. Processing a single
instruction
def processInstruction(i: Instruction)(s: Playground):
Playground = {!
val next = i match {!
case A => s.r1.currentPosition.move(s)!
case i => s.r1.currentPosition.turn(i)!
}!
!
if (s.coins.contains(next.point)) {!
s.copy(!
coins = s.coins - next.point, !
r1 = s.r1.addCoin(next.point).addPosition(next)!
)!
} else {!
s.copy(r1 = s.r1.addPosition(next))!
}!
}
43. Processing a single
instruction
def processInstruction(i: Instruction)(s: Playground):
Playground = {!
val next = i match {!
case A => s.r1.currentPosition.move(s)!
case i => s.r1.currentPosition.turn(i)!
}!
We always process the robot on first position !
!
Robots will be swapped alternatively.
if (s.coins.contains(next.point)) {!
s.copy(!
coins = s.coins - next.point, !
r1 = s.r1.addCoin(next.point).addPosition(next)!
)!
} else {!
s.copy(r1 = s.r1.addPosition(next))!
}!
}
44. Quick reminder
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = State { s =>!
val (s1, a) = run(s)!
(s1, f(a))!
}!
def flatMap[B](f: A => State[S, B]): State[S, B] = !
State { s =>!
val (s1, a) = run(s)!
f(a).run(s1)!
}!
}!
object State {!
def apply[S, A](f: S => (S, A)): State[S, A] =!
new State[S, A] {!
def run(initial: S): (S, A) = f(initial)!
}!
45. Introducing new
combinators
trait State[S, +A] {!
...!
}!
object State {!
def apply[S, A](f: S => (S, A)): State[S, A] =!
new State[S, A] {!
def run(initial: S): (S, A) = f(initial)!
}!
!
def get[S]: State[S, S] = State { s => (s, s) }!
!
def gets[S, A](f: S => A): State[S, A] = !
State { s => (s, f(s)) }!
}
46. Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Score, Score)] = i1 match {!
case Nil if i2 == Nil => State.gets(_.scores)!
case Nil => State[Playground, (Score, Score)] { s =>
(s.swapRobots(), s.scores) !
}.flatMap { _ => compileInstructions(i2, i1) }!
case head::tail => State[Playground, (Score, Score)] !
{ s =>!
val s1 = processInstruction(head)(s)!
(s1.swapRobots(), s1.scores)!
}.flatMap { _ => compileInstructions(i2, tail) }!
}
!
47. Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Score, Score)] = i1 match {!
case Nil if i2 == Nil => State.gets(_.scores)!
case Nil => State[Playground, (Score, Score)] { s =>
(s.swapRobots(), i1 and i2 are!
s.scores) empty, we return a State
If both
}.flatMap { _ => compileInstructions(i2, i1) }!
Monad with the run method implementation :
case head::tail => State[Playground, (Score, Score)] !
s => (s, s.scores)!
{ s =>!
This will return the Playground passed in argument
val s1 = processInstruction(head)(s)!
and the score as result.
(s1.swapRobots(), s1.scores)!
}.flatMap { _ => compileInstructions(i2, tail) }!
}
!
48. Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Score, Score)] = i1 match {!
case Nil if i2 == Nil => State.gets(_.scores)!
case Nil => State[Playground, (Score, Score)] { s =>
(s.swapRobots(), s.scores) !
}.flatMap { _ => compileInstructions(i2, Nil) }!
case head::tail => State[Playground, (Score, Score)] !
{ s =>! If i1 is empty, we return a State Monad with a run
val s1 method that swap robots in Playground and returns
= processInstruction(head)(s)!
(s1.swapRobots(), s1.scores)!
scores.
}.flatMap { _we chain it with the processing of instructions for
Then => compileInstructions(i2, tail) }!
}
the second list.
!
49. Here comes the magic !
def compileInstructions(!
i1: List[Instruction], i1 and return a new Playground where
We process !
i2: List[Instruction]!
robots are swapped.
): State[Playground, (Score, Score)] = i1 matchinstructions
Then we chain it with the processing of the {!
case Nil if i2 == Nil of i1.
i2 and tail => State.gets(_.scores)!
case Nil => State[Playground, (Score, Score)] { !s =>
Lists of instructions are processed alternatively
(s.swapRobots(), s.scores) !
}.flatMap { _ => compileInstructions(i2, i1) }!
case head::tail => State[Playground, (Score, Score)] !
{ s =>!
val s1 = processInstruction(head)(s)!
(s1.swapRobots(), s1.scores)!
}.flatMap { _ => compileInstructions(i2, tail) }!
}
!
50. Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Score, Score)] = i1 match {!
case Nil if i2 == Nil => State.gets(_.scores)!
case Nil => State[Playground, (Score, Score)] { s =>
(s.swapRobots(), s.scores) !
}.flatMap { _ => compileInstructions(i2, i1) }!
case head::tail => State[Playground, (Score, Score)] !
{ s =>!
val s1 = processInstruction(head)(s)!
(s1.swapRobots(), s1.scores)!
}.flatMap { _ => compileInstructions(i2, tail) }!
}
!
52. Conclusion
•
State Monad simplify computations on states
•
Use it whenever you want to manipulate states in a
purely functional (parsing, caching, validation ...)
53. To learn more about State
Monad
•
Functional programming in Scala by Paul Chiusano
and Rúnar Bjarnason - This book is awesome !
•
State Monad keynote by Michael Pilquist - https://
speakerdeck.com/mpilquist/scalaz-state-monad
•
Learning scalaz by Eugene Yokota - http://
eed3si9n.com/learning-scalaz/State.html