Playing with the State Monad
David Galichet	

Freelance Developer

Twitter : @dgalichet
Wait ! what’s a Monad ?
•

Functor

•

Monad

•

For comprehensions
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
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. Id...
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):...
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 t...
Functor
trait
def
def
def
}

Functor[F[+_]] {!
pure[A](a: A): F[A]!
map[A,B](fa: F[A])(f: A => B): F[B]!
lift[A,B](f: A =>...
Functor
trait
def
!
def
!
def
}

Functor[F[+_]] {!
pure[A](a: A): F[A]!

Functor and Monads are also
known as typeclasses
...
Functor
trait
def
!
def
!
def
{ fa:
!

}

Functor[F[_]] {!
pure[A](a: A): F[A]!
map[A,B](fa: F[A])(f: A => B): F[B]!
lift[...
Ex: Maybe is a Functor
sealed trait Maybe[+A]!
case class Value[+A](a: A) extends Maybe[A]!
case object Empty extends Mayb...
Ex: Maybe is a Functor
We define a generic function that double the content of a
Functor :
import monads.MaybeIsAFunctor!
d...
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...
Monad
and methods unit and bind implement the following
laws :
1. Left Identity : bind(unit(x))(f) == f(x) !
2. Right Iden...
Monad
trait Monad[M[+_]] extends Functor[M] {!
def unit[A](a: A): M[A] = pure(a)!
!

def bind[A, B](ma: M[A])(f: A => M[B]...
Ex : Maybe is a Monad
implicit val maybeIsAMonad = new Monad[Maybe] {!
def pure[A](a: A) = Value(a)!
!

def map[A, B](fa: ...
Ex : Maybe is a Monad
implicit val maybeIsAMonad = new Monad[Maybe] {!
def pure[A](a: A) = Value(a)!
!

def map[A, B](fa: ...
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[In...
For comprehension
•

Scala provides For Comprehension to simplify
chaining of map and flatMap (equivalent to do
notation in...
For comprehension
Scala For Comprehension needs that map and
flatMap to be defined on object directly (not using
typeclass)....
For comprehension
import monads.maybeIsAMonad!
!

def add2[M[+_]](ma: M[Int], mb: M[Int])(implicit MA:
Monad[M]): M[Int] =...
Generic programming
def sequence[A, M[+_]](ms: List[M[A]])(implicit MA:
Monad[M]): M[List[A]] = ms match {!
case Nil => MA...
Let’s continue our journey
with a simple problem
3
2
1
0
0

1

2

3
Rules of the game
•

We want to simulate two robots moving through a
nxm playground

•

Each robot can either turn on a di...
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

...
Think about this game
•

It appears that we will deal with many states :
•

Playground with its coins

•

Robots with thei...
We want functional purity
•

Functional Purity has many advantages like
composability, idempotence, maintainability and
th...
Dealing with states
S => (S, A)
•

S is the type of a state and A the type of a
computation

•

The outcome of this functi...
Chaining states
computations
def chainStOps(!
c1: S => (S, A), !
c2: S => (S, A)!
): S => (S, A) = { s =>!
val (s1, _) = c...
Introducing State Monad

The aim of the state monad is to abstract over state
manipulations
Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = ???!
def f...
Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = ???!
def f...
Introducing State Monad
trait State[S, +A] {!
def run(initial: S): (S, A)!
!

def map[B](f: A => B): State[S, B] = State {...
Introducing State Monad

trait State[S, +A] {!
def run(initial: S): (S, A)!
!

def map[B](f: A => B): State[S, B] = State ...
Coming back to our game !
•

We drive robots using a list of instructions

sealed trait Instruction!
case object L extends...
Coming back to our game !
•

Each robot has a direction

sealed trait Direction {!
def turn(i: Instruction): Direction!
}!...
Coming back to our game !
•

A direction and a location define a position

case class Point(x: Int, y: Int)!
!

case class ...
Coming back to our game !
•

And each Robot is a player with a Score

sealed trait Player!
case object R1 extends Player!
...
Coming back to our game !
•

The state of each Robot is defined as :

case class Robot(!
player: Player, !
positions: List[...
Coming back to our game !
•

Robots evolve in a playground :

case class Playground(!
bottomLeft: Point, topRight: Point, ...
Look what we did
•

a set of Instructions,

•

a Position composed with Points and Direction,

•

a definition for Players ...
Let put these all together !
•

Now, we need a method to process a single
instruction

•

And a method to process all inst...
Processing a single
instruction
def processInstruction(i: Instruction)(s: Playground):
Playground = {!
val next = i match ...
Processing a single
instruction
def processInstruction(i: Instruction)(s: Playground):
Playground = {!
val next = i match ...
Quick reminder
trait State[S, +A] {!
def run(initial: S): (S, A)!
def map[B](f: A => B): State[S, B] = State { s =>!
val (...
Introducing new
combinators
trait State[S, +A] {!
...!
}!
object State {!
def apply[S, A](f: S => (S, A)): State[S, A] =!
...
Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Sco...
Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Sco...
Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Sco...
Here comes the magic !
def compileInstructions(!
i1: List[Instruction], i1 and return a new Playground where
We process !
...
Here comes the magic !
def compileInstructions(!
i1: List[Instruction], !
i2: List[Instruction]!
): State[Playground, (Sco...
Using for comprehensions
def getPositions(p: Playground): (Position, Position) =
(p.r1.currentPosition, p.r2.currentPositi...
Conclusion

•

State Monad simplify computations on states

•

Use it whenever you want to manipulate states in a
purely f...
To learn more about State
Monad
•

Functional programming in Scala by Paul Chiusano
and Rúnar Bjarnason - This book is awe...
Questions ?
Upcoming SlideShare
Loading in...5
×

Introducing Monads and State Monad at PSUG

805

Published on

0 Comments
5 Likes
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total Views
805
On Slideshare
0
From Embeds
0
Number of Embeds
0
Actions
Shares
0
Downloads
22
Comments
0
Likes
5
Embeds 0
No embeds

No notes for slide

Introducing Monads and State Monad at PSUG

  1. 1. Playing with the State Monad David Galichet Freelance Developer Twitter : @dgalichet
  2. 2. Wait ! what’s a Monad ? • Functor • Monad • For comprehensions
  3. 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. 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. 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. 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)
  7. 7. Functor trait def def def } Functor[F[+_]] {! pure[A](a: A): F[A]! map[A,B](fa: F[A])(f: A => B): F[B]! lift[A,B](f: A => B): F[A] => F[B] = ???!
  8. 8. Functor trait def ! def ! def } Functor[F[+_]] {! pure[A](a: A): F[A]! Functor and Monads are also known as typeclasses map[A,B](fa: F[A])(f: A => B): F[B]! lift[A,B](f: A => B): F[A] => F[B] = ???!
  9. 9. Functor trait def ! def ! def { fa: ! } Functor[F[_]] {! pure[A](a: A): F[A]! map[A,B](fa: F[A])(f: A => B): F[B]! lift[A,B](f: A => B): F[A] => F[B] = ! F[A] => map(fa)(f) }!
  10. 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. 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. 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. 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) }
  14. 14. Monad trait Monad[M[+_]] extends Functor[M] {! def unit[A](a: A): M[A] = pure(a)! ! def bind[A, B](ma: M[A])(f: A => M[B]): M[B]! ! def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] = bind(ma)(f)! }!
  15. 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. 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. 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. 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. 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. 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. 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))
  22. 22. Let’s continue our journey with a simple problem 3 2 1 0 0 1 2 3
  23. 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. 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. 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. 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. 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
  28. 28. Chaining states computations def chainStOps(! c1: S => (S, A), ! c2: S => (S, A)! ): S => (S, A) = { s =>! val (s1, _) = c1(s)! c2(s1)! } Repeated many times, this can be error prone !
  29. 29. Introducing State Monad The aim of the state monad is to abstract over state manipulations
  30. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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. 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) }! } !
  51. 51. Using for comprehensions def getPositions(p: Playground): (Position, Position) = (p.r1.currentPosition, p.r2.currentPosition)! ! def enhanceResult(! i1: List[Instruction], ! i2: List[Instruction]): State[Playground, (String, (Position, Position))] = {! for {! scores <- compileInstructions(i1, i2)! positions <- State.gets(getPositions)! } yield (declareWinners(scores), positions)! }
  52. 52. Conclusion • State Monad simplify computations on states • Use it whenever you want to manipulate states in a purely functional (parsing, caching, validation ...)
  53. 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
  54. 54. Questions ?
  1. A particular slide catching your eye?

    Clipping is a handy way to collect important slides you want to go back to later.

×