Looking at OO for a Pattern
We'll look at three examples of OO code to help us understand it's equivalent in FP code.
Incrementing an Integer
Given this code:
a = 0;
x = a++;
y = a++;
z = a++;
// which we'll rewrite to use a function that receives an argument
a = 0;
x = getAndIncrement(a);
y = getAndIncrement(a);
z = getAndIncrement(a);
getAndIncrement
is an example of an impure function because it does not return the same value each time it is called. Moreover, a
's value changes over time, so that a /= 0
at the end of our program. How might we write the same thing using pure functions? We'll demonstrate a few attempts and explain their problems before showing the final working solution
// we'll make the function pure
// and call it "add1"
var add1 = (i) => i + 1;
a = 0;
x = add1(a);
y = add1(a);
z = add1(a);
// Values end states are:
a == 0
x == y == z == 1
The problem is add1
receives the wrong state as an argument. If we pass the returned state from our previous call into the next call, we can resolve this problem:
a = 0;
x = add1(a);
y = add1(x);
z = add1(y);
// Values end states are:
a == 0
x == 1
y == 2
z == 3
At this point, we could do state manipulation using a recursive function...
runNTimes :: forall a. Int -> (a -> a) -> a -> a
runNTimes 0 _ output = output
runNTimes count func arg = runNTimes (count - 1) func (func arg)
... but state manipulation is more complicated than that. What if we wanted to add 1 at one point and add 2 at another? What if we want to subtract 5 as well? In short, this approach does not work when we increase the complexity of the state manipulation. The next two examples will focus on a different kind of state manipulation.
Random Number Generators
Given this code:
x = random.nextInt
y = random.nextInt
z = random.nextInt
// rewritten to use "function arg" syntax
x = nextInt(random);
y = nextInt(random);
z = nextInt(random);
nextInt
is an impure function because it does not return the same value each time it is called. How might we write the same thing using pure functions? We'll demonstrate a few attempts and explain their problems before showing the final working solution
// Assume that `nextInt` is now pure...
x = nextInt(random);
y = nextInt(random);
// ... then 'x' ALWAYS equals 'y'
// A random number can sometimes be the same one as before,
// but this shouldn't always be true
// To make `x /= y`, we need a new `random` value, something like:
x = nextInt(random1);
y = nextInt(random2);
The solution is to make nextInt
return two things via the Tuple a b
type
- the random int value
- a new value of
random
(Tuple x random2) = nextInt(random1);
(Tuple y random3) = nextInt(random2);
where Tuple a b
is just a box that holds two values of the same/different types:
data Tuple a b = Tuple a b
Popping Stacks
We'll explain this idea once more using a different context: Stacks. In OO, we can write the following code:
// assuming we have a non-empty stack:
// (top) [1, 2, 3, 4, 5] (bottom)
x = stack.pop // x == 1
y = stack.pop // y == 2
z = stack.pop // z == 3
// rewritten using "function arg" syntax
x = pop(stack);
y = pop(stack);
z = pop(stack);
pop
is an impure function as calling it will not return the same value each time it is called. How might we write the same thing using pure functions?
// Assume that `nextInt` is now pure...
x = pop(stack);
y = pop(stack);
// ... we just popped the same value twice off of the stack
// so that 'x' is always the same value/object as 'y'
// In other words
pop(stack) == x == y == 1
// To make `y` == 2, we need a version of `stack` that will return `2`
// as its next value to `pop`. In other words, something like...
x = pop(originalStack);
y = pop(originalStack_withoutX);
The solution is to make pop
return two things via the Tuple a b
type:
- the popped value
- a new version of
stack
without the popped value
(Tuple x originalStack_withoutX) = pop(originalStack);
(Tuple y originalStack_withoutXorY) = pop(originalStack_withoutX);
Identifying the Pattern
Here's the solution we came up with:
(Tuple x random2) = randomInt(random1);
(Tuple y random3) = randomInt(random2);
(Tuple x originalStack_withoutX) = pop(originalStack);
(Tuple y originalStack_withoutXorY) = pop(originalStack_withoutX);
// and generalizing it to a pattern, we get
(Tuple value1, value2 ) = stateManipulation(value1);
(Tuple value2, value3 ) = stateManipulation(value2);
(Tuple value3, value4 ) = stateManipulation(value3);
// ...
(Tuple value_N, value_N_plus_1) = stateManipulation(valueN);
Turning this into Purescript syntax, we get:
state_manipulation_function :: forall state value. (state -> Tuple value state)