James Earl Douglas
October 13, 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 RW 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
}
def withdraw(x: Float): Float = {
account = account :+ (-x)
account.sum
}
account
reference can change any timedeposit
or withdraw
makes the transaction happenaccount
reference is accessed multiple
timesdef deposit(x: Float): List[Float] => (Float, List[Float]) =
{ account => (account.sum, account :+ x) }
def withdraw(x: Float): List[Float] => (Float, List[Float]) =
{ account => (account.sum, account :+ (-x)) }
account
reference can not changedeposit
or
withdraw
does not run the transactionaccount
accesses are certain to be
identicalA State
wraps a function run
which, given a
state S
, performs some computation and produces both an
A
and a new state 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:
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.