You have a mutable variable (a var
in Scala) that is
both read from and written to outside of a tightly-scoped block.
Remodel the block as functions that take an initial value of the variable, and return both the final value of the variable and the original return value of the expression.
var x = 6
println(s"x = ${x}") // x = 6
= x * 7
x println(s"x = ${x}") // x = 42
We use the following form for our state monad:
case class State[A,S](val run: S => (A,S)) {
def flatMap[B](f: A => State[B,S]): State[B,S] =
State { s =>
val (a,s2) = run(s)
f(a).run(s2)
}
}
We can use flatMap
to implement other helpful
methods:
implicit class StateExtras[A,S](s: State[A,S]) {
def andThen[B](x: State[B,S]): State[B,S] =
.flatMap { _ => x }
sdef map[B](f: A => B): State[B,S] =
.flatMap { a =>
sState { s =>
(f(a),s)
}
}
}
val six: State[Unit,Int] = State { _ => ( (), 6) }
val print: State[Unit,Int] = State { x => (println(s"x = ${x}"), x) }
val times7: State[Unit,Int] = State { x => ( (),x*7) }
val sixBy7: State[Unit,Int] = six andThen print andThen times7 andThen print
-999 // x = 6
sixBy7 run // x = 42
Mutable variables tend to preclude referential transparency, and can lead to all kind of bugs related to timing, threading, parallelism, lack of idempotency, evaluation order, lack of equational reasoning, etc.
The state monad models a mutable state change in a referentially transparent way. With it we can represent data mutation as a functional data structure.
When encountering a mutable variable, note where it's read from and written to:
var greeting: String = ""
val user = System.getenv("USER")
= s"Greetings, ${user}!"
greeting
val osName = System.getProperty("os.name")
= s"${greeting} ${osName}, eh? Solid."
greeting
println(greeting)
Modularize these reads and writes into discrete functions.
Each function takes an incoming version of the variable, and returns some value along with an outgoing version of the variable.
def getUser(greeting: String): (String,String) = {
val user = System.getenv("USER")
(user, s"Greetings, ${user}!")
}
def getOSName(greeting: String): (String,String) = {
val osName = System.getProperty("os.name")
(osName, s"${greeting} ${osName}, eh? Solid.")
}
def getGreeting(greeting: String): (String,String) =
(greeting, greeting)
To make these explicitly composable, wrap each using the state monad:
val getUserS: State[String,String] =
State { getUser }
val getOSNameS: State[String,String] =
State { getOSName }
val getGreetingS: State[String,String] =
State { getGreeting }
"" getUserS andThen getOSNameS andThen getGreetingS map println run
When it's preferable to code in terms of mutable references, we can use a data structure that wraps a mutable variable and represents its access and mutation using the state monad.
This data structure is frequently called STRef
, or
StateRef
:
class StateRef[A,S](_a: A) {
private var a: A = _a
def read: State[A,S] =
State(s => (a,s))
def write(a2: A): State[Unit,S] =
State({s =>
= a2
a ((),s)
})
}
object StateRef {
def apply[S,A](a: A): State[StateRef[A,S],S] =
State(s => (new StateRef(a),s))
}
We can use one or more StateRef
s together to code in
terms of mutable references, while keeping the desired characteristics
of the state monad:
val nameS: State[String,Unit] =
for {
<- StateRef("James")
firstSR <- StateRef("Earl")
middleSR <- StateRef("Douglas")
lastSR <- StateRef("")
nameSR <- firstSR.read
first <- middleSR.read
middle <- middleSR.write("Buster") // does not overwrite `middle`
_ <- lastSR.read
last <- nameSR.write(s"${first} ${middle} ${last}")
_ <- nameSR.read
name } yield name
run () nameS map println
Greetings, james! Linux, eh? Solid.
Greetings, james! Linux, eh? Solid.
James Earl Douglas