You are on page 1of 10

Purely Functional State

juanmanuel.gimeno@udl.cat

2023

Index
• Purely functional random number generation
• Working with stateful APIs
• The State data type
• Purely functional imperative programming

Generating random numbers using side effects


• Even that random generation is not the most compelling example, it is a
simple domain in which present the techniques for dealing with stateful
APIs in a pure functional way
scala> val rng = new scala.util.Random

scala> rng.nextDouble
res1: Double = 0.9867076608154569

scala> rng.nextDouble
res2: Double = 0.8455696498024141

scala> rng.nextInt
res3: Int = -623297295

scala> rng.nextInt(10)
res4: Int = 4

scala> rng.nextInt
res5: Int = 75432356
• Clearly the code shown above is not referentially transparent

1
Purely functional random number generation
• The key idea to recover referential transparency is to make state updates
explicit
– don’t update the hidden state as a side effect
– return the new state along with the generated value
trait RNG:
def nextInt: (Int, RNG)
• Let’s define an implementation using a linear congruent generator
case class SimpleRNG(seed: Long) extends RNG:
def nextInt: (Int, RNG) =
val newSeed = (seed * 0x5DEECE66DL + 0xBL) & 0xFFFFFFFFFFFFL
val nextRNG = SimpleRNG(newSeed)
val n = (newSeed >>> 16).toInt
(n, nextRNG)

Purely functional random number generation


• We can use this generator in the REPL
scala> val rng = SimpleRNG(42)
rng: SimpleRNG = SimpleRNG(42)

scala> val (n1, rng2) = rng.nextInt


n1: Int = 16159453
rng2: RNG = SimpleRNG(1059025964525)

scala> val (n2, rng3) = rng2.nextInt


n2: Int = -1281479697
rng3: RNG = SimpleRNG(197491923327988)
• In general

2
(From
FP in Scala (2nd ed), Chapter 6)

Making stateful APIs pure


• We can deal with stateful APIs in this way, i.e. making the API compute
the next state rather than mutating anything.
• From
class Foo:
private var s: FooState = ...
def bar: Bar
def baz: Int
to
trait Foo:
def bar: (Bar, Foo)
def baz: (Int, Foo)

Making stateful APIs pure


• When we use this pattern, we make the caller responsible for passing the
computed next state to the rest of the program
• This is bad

3
def randomPair(rng: RNG): (Int,Int) =
val (i1,_) = rng.nextInt
val (i2,_) = rng.nextInt
(i1,i2)
• This is ok
def randomPair(rng: RNG): ((Int,Int), RNG) =
val (i1,rng2) = rng.nextInt
val (i2,rng3) = rng2.nextInt
((i1,i2), rng3)

Exercises
• Write a function that uses RNG.nextInt to generate a random integer
between 0 and Int.MaxValue (inclusive). Make sure to handle the corner
case when nextInt returns Int.MinValue, which doesn’t have a non-
negative counterpart.
– def nonNegativeInt(rng: RNG): (Int, RNG)
• Write a function to generate a Double between 0 and 1, not including 1.
– Use Int.MaxValue and .toDouble
– def double(rng: RNG): (Double, RNG)
• Write functions to generate an (Int, Double) pair, a (Double, Int)
pair, and a (Double, Double, Double)
– def intDouble(rng: RNG): ((Int, Double), RNG)
– def doubleInt(rng: RNG): ((Double, Int), RNG)
– def double3(rng: RNG): ((Double, Double, Double), RNG)
• Write a function to generate a list of random integers.
– def ints(count: Int)(rng: RNG): (List[Int], RNG)

A better API for state actions


• All of the above functions share a common pattern
– They’re of the form: RNG => (A, RNG) for some type A
• Functions of these type are called state actions or state transitions
because they trensform RNG states from one to the next
– They can be combined using combinators which are HOFs that will
simplify dealing with them (e.g. threading the state)
• Let’s first define an alias for the RNG state action data type:

4
type Rand[+A] = RNG => (A, RNG)
• We can turn methods into values of this new type
val int: Rand[Int] = _.nextInt
• We want to write combinators that let us combine Rand actions while
avoiding explicitly passing along the RNG state.
– We’ll end up with a kind of domain-specific language (DSL) that
does all of the passing for us
– The unit action, which passes the RNG state through without using
it, always returning a constant value rather than a random value
def unit[A](a: A): Rand[A] =
rng => (a, rng)
– There’s also map for transforming the output of a state action without
modifying the state itself
def map[A, B](s: Rand[A])(f: A => B): Rand[B] =
rng => {
val (a, rng2) = s(rng)
(f(a), rng2)
}
– For example we can use map to generate non-negative even numbers
def nonNegativeEven: Rand[Int] =
map(nonNegativeInt)(i => i - i % 2)

Exercises: Combining state actions


• This function takes two actions, ra and rb, and a function f for combining
their results, and returns a new action that combines them
– def map2[A, B, C](ra: Rand[A], rb: Rand[B])(f: (A, B) =>
C): Rand[C]
• Use map2 to define
– def both[A, B](ra: Rand[A], rb: Rand[B]): Rand[(A, B)]
• Use both to reimplement intDouble and doubleInt
– val randIntDouble: Rand[(Int, Double)] = ???
– val randDoubleInt: Rand[(Double, Int)] = ???
• Implement sequence for combining a List of transitions into a single
transition
– def sequence[A](rs: List[Rand[A]]): Rand[List[A]]
• Use it to reimplement the ints function you wrote before.
– HINT: use the standard library function List.fill(n)(x) to make a list
with x repeated n times.
– def ints(count: Int): Rand[List[Int]]

5
Nesting state actions
• If we analyse the solutions to the exercises we are creating a series of
combinators (map and map2) that allows us to implement functions without
having to thread the state explicitly
• But there are functions that, with only those combinators, we cannot
implement
• One such function is nonNegativeLessThan, which generates an integer
between 0 (inclusive) and n (exclusive)
def nonNegativeLessThan(n: Int): Rand[Int]
• We can try with
def nonNegativeLessThan(n: Int): Rand[Int] =
map(nonNegativeInt)(_ % n)
but it’ll be skewed because Int.MaxValue may not be exactly divisible by
n, so numbers that are less than the remainder of that division will come
up more frequently.

Nesting state actions


• When nonNegativeInt generates numbers higher than the largest multiple
of n that fits in a 32-bit integer, we should retry the generator and hope
to get a smaller number. We might attempt this:
def nonNegativeLessThan(n: Int): Rand[Int] =
map(nonNegativeInt) { i =>
val mod = i % n
if i + (n-1) - mod >= 0 then mod else nonNegativeLessThan(n)(???)
}
but nonNegativeLessThan(n) has the wrong type to be used right there

• What we would like is to chain things together so that the RNG that’s
returned by nonNegativeInt is passed along to the recursive call to
nonNegativeLessThan.

6
– We could pass it along explicitly instead of using map, like this:
def nonNegativeLessThan(n: Int): Rand[Int] = { rng =>
val (i, rng2) = nonNegativeInt(rng)
val mod = i % n
if i + (n-1) - mod >= 0 then
(mod, rng2)
else nonNegativeLessThan(n)(rng2)
}
but it would be better to have a combinator that does this passing along
for us

Exercise
• Implement flatMap
– def flatMap[A, B](r: Rand[A])(f: A => Rand[B]): Rand[B]
• Use it to implement nonNegativeLessThan
– def nonNegativeLessThan(n: Int): Rand[Int]
• Reimplement map and map2 in terms of flatMap
– NOTE: The fact that this is possible is what we’re referring to when
we say that flatMap is more powerful than map and map2

A general state action data type


• unit, map, map2, flatMap and sequence aren’t really specific to random
number generation at all.
• They’re general-purpose functions for working with state actions, and don’t
care about the type of the state
• We should then come up with a more general type than Rand, for handling
any type of state
type State[S, +A] = S => (A, S)
• Here State is short for computation that carries some state along,
or state action, state transition, or even statement
• We might want to write it as its own type instead of using an alias
– This allows us to define methods without worrying about conflicting
with the methods on the function type
case class State[S, +A](run: S => (A, S))
• Alternatively, we can use a feature of the Scala type system called opaque
types.
– Inside the defining scope an opaque type behaves like a type alias

7
– Outside of the defining scope anthe opaque type is unrelated to
the representation type
opaque type State[S, +A] = S => (A, S)

object State:
extension [S, A](underlying: State[S, A])
def run(s: S): (A, S) = underlying(s)

def apply[S, A](f: S => (A, S)): State[S, A] = f


• We can define now Rand as:
type Rand[A] = State[RNG, A]

Exercises
• Generalize the functions unit, map, map2, flatMap, and sequence.
– Add them as extension methods on the State type where possible.
– Otherwise you should put them in a State companion object.

Purely functional imperative programming


• In the imperative programming paradigm, a program is a sequence
of statements where each statement may modify the program state
– And that’s what we’ve been doing with our state actions !!!
• Consider the following function:
val ns: Rand[List[Int]] =
int.flatMap(x =>
int.flatMap(y =>
ints(x).map(xs =>
xs.map(_ % y))))
• Seems messy but, as we have defined map and flatMap, we can use a
for-comprehension
val ns: Rand[List[Int]] =
for
x <- int
y <- int
xs <- ints(x)
yield xs.map(_ % y)
• This code is much easier to read (and write), and it looks like it is an
imperative program that maintains some state.

8
Purely functional imperative programming
• To facilitate this kind of imperative programming with we really only need
two primitive State combinators
– get: for reading the state and one for writing the state.
∗ def get[S]: State[S, S] = s => (s, s)
– set: for setting a new state
∗ def set[S](s: S): State[S, Unit] = _ => ((), s)
• For instance, we could use them to implement a modify combinator
def modify[S](f: S => S): State[S, Unit] =
for
s <- get
_ <- set(f(s))
yield ()

Exercise
Implement a finite state automaton that models a simple candy dispenser
• The machine has two types of input:
– insert a coin
– turn the knob to dispense candy.
• It can be in one of two states:
– locked
– unlocked.
• It also tracks how many candies are left and how many coins it contains.
enum Input:
case Coin, Turn

case class Machine(locked: Boolean, candies: Int, coins: Int)


• The rules of the machine are as follows:
– Inserting a coin into a locked machine will cause it to unlock if there’s
any candy left.
– Turning the knob on an unlocked machine will cause it to dispense
candy and become locked.
– Turning the knob on a locked machine or inserting a coin into an
unlocked machine does nothing.
– A machine that’s out of candy ignores all inputs.
• The method simulateMachine should operate the machine based on the list
of inputs and return the number of coins and candies left in the machine
at the end.
– def simulateMachine(inputs: List[Input]): State[Machine,
(Int, Int)]

9
Bibliography
• Michael Pilquist, Rúnar Bjarnason and Paul Chiusano, “Functional Pro-
gramming in Scala, Second Edition”, Manning Publications (MEAP)
– Chapter 6
• Paul Chiusano, Rúnar Bjarnason, “Functional Programming in Scala”,
Manning Publications (2005)
• Exercises, hints, and answers for the book Functional Programming in
Scala

10

You might also like