James Earl Douglas
June 26, 2014
This is an adaptation of Real-World Functional Programming.
Functional programming*
* consider FP ≈ referential transparency
Real-world**
** for readability, we'll use a simplified (but analogous) example
Consider a banking ATM. A user should be able to:
Accurate handling of state is kind of important, because money.
We'll track a bank account as a simple transaction register.
Float
List[Float]
var account: List[Float] = ...
def deposit(x: Float): Float = {
account = account :+ x
account.sum
}
account
reference can change any timedeposit
makes the transaction
happenaccount
reference is accessed multiple
timesdef deposit(x: Float): List[Float] => (Float, List[Float]) =
{ account => (account.sum, account :+ x) }
account
reference can not changedeposit
does not run the
transactionaccount
accesses are certain to be
identicalA State
wraps a function run
which, given
an S
, performs some computation and produces both an
A
and a new S
.
case class State[S,A](run: S => (A,S)) {
def map[B](f: A => B): State[S,B] =
State { run andThen { case (a,s) => (f(a), s) } }
}
map
builds a new state action that, once run, has its
output run through f
, converting it from an A
to a B
.
case class State[S,A](run: S => (A,S)) {
def flatMap[B](f: A => State[S,B]): State[S,B] =
State { run andThen { case (a,s) => f(a).run(s) } }
}
flatMap
builds a new state action that, once run, uses
the output to compute the result of another state action.
var account = ...
)account.append(...)
)* Depending on the later implementation of an interpreter
def deposit(x: Float): Tx[(Float,Float)] =
for {
_ <- contribute(x) // Tx[Unit]
b <- balance // Tx[Float]
} yield (0,b)
def withdraw(x: Float): Tx[(Float,Float)] =
for {
w <- deduct(x) // Tx[Float]
b <- balance // Tx[Float]
} yield (w,b)
def depositThenWithdraw(d: Float, w: Float): Tx[(Float,Float)] =
for {
_ <- deposit(d) // Tx[(Float,Float)]
w <- withdraw(w) // Tx[(Float,Float)]
} yield w
To make it go, we need a way to run a state action:
var account: List[Float] = Nil
def run(x: Tx[(Float,Float)]): ((Float,Float),List[Float]) = {
val ((w,b),a) = x.run(account)
account = a
((w,b),a)
}
Uh oh, there's that pesky var
again...
We can't escape mutability, but we can push it to the outer edges of our program, and tailor the interpreter to the mechanism of state persistence.