Practical Functional Programming in JavaScript - Side Effects and Purity
Richard Tong
Posted on August 3, 2020
Today we'll discuss two fundamental qualities of JavaScript functions and systems: side effects and purity. I also demonstrate an approach to organizing programs around these qualities with a couple of functions from my functional programming library, rubico.
A function is pure if it satisfies the following conditions:
- Its return value is the same for the same arguments
- Its evaluation has no side effects
A function's side effect is a modification of some kind of state beyond a function's control - for instance:
- Changing the value of a variable;
- Writing some data to disk;
- Enabling or disabling a button in the User Interface.
Here are some more instances of side effects
- reading data from a file
- making a request to a REST API
- writing to a database
- reading from a database
- logging out to console
Indeed, console.log
is a side-effecting function.
// console.log(message string) -> undefined
console.log('hey') // undefined
In pure math terms, console.log
takes a string and returns undefined, which isn't so useful. However, console.log
is very useful in practice because of its side effect: logging any arguments you pass it out to the console. I like console.log
because it only does one thing and does it well: log stuff out to the console. When the most straightforward solutions to real life challenges involve a mixture of side-effects and pure computations at a similar execution time, it's useful to have functions like console.log
that have isolated, predictable behavior. My opinion is it's misguided to try to temporally separate side-effects and pure computations in JavaScript for the sake of mathematical purity - it's just not practical. Rather, my approach is to isolate side effects to simple functions like console.log
.
I'll demonstrate with a function add10
with several different side effects. add10
is not pure.
let numCalls = 0
const add10 = number => {
console.log('add10 called with', number)
numCalls += 1
console.log('add10 called', numCalls, 'times')
return number + 10
}
add10(10) /*
add10 called with 10
add10 called 1 times
20
*/
add10
has the side effects of logging out to the console, mutating the variable numCalls
, and logging out again. Both console.log
statements have side effects because they use the function console.log
, which has the side effect of logging out to the console. The statement numCalls += 1
also has a side effect because the variable numCalls
is state beyond the control of the function.
By refactoring the console logs and the variable mutation to an outside function add10WithSideEffects
, we can have a pure add10
.
let numCalls = 0
const add10 = number => number + 10
const add10WithSideEffects = number => {
console.log('add10 called with', 10)
numCalls += 1
console.log('add10 called', numCalls, 'times')
return add10(10)
}
add10WithSideEffects(10) /*
add10 called with 10
add10 called 1 times
20
*/
Keep in mind that while add10
is now pure, all we've done is move our side effects outside the scope of add10
and into the more explicit add10WithSideEffects
. Now we're being explicit about the side effects at least, but it's still a bit messy in my eyes. As far as vanilla JavaScript goes, this code is fine. However, I think we can get cleaner with my functional programming library, rubico.
The functions are simple enough at their core so that if you don't want to use a library, you can take these versions of the functions in vanilla JavaScript. Introducing: pipe
and tap
/**
* @name pipe
*
* @synopsis
* pipe(funcs Array<function>)(value any) -> result any
*/
const pipe = funcs => function pipeline(value) {
let result = value
for (const func of funcs) result = func(result)
return result
}
/**
* @name tap
*
* @synopsis
* tap(func function)(value any) -> value
*/
const tap = func => function tapping(value) {
func(value)
return value
}
-
pipe takes an array of functions and chains them all together, calling the next function with the previous function's output. We'll use
pipe
as a base foundation to organize our side effects. -
tap takes a single function and makes it always return whatever input it was passed. When you use
tap
on a function, you're basically saying "don't care about the return from this function, just call the function with input and give me back my input".tap
is great for functions responsible for a single side-effect likeconsole.log
. We'll usetap
to separate our side-effects by function.
const logCalledWith = number => console.log('add10 called with', number)
let numCalls = 0
const incNumCalls = () => numCalls += 1
const logNumCalls = () => console.log('add10 called', numCalls, 'times')
const add10 = number => number + 10
const add10WithSideEffects = pipe([
tap(logCalledWith),
tap(incNumCalls),
tap(logNumCalls),
add10,
])
add10WithSideEffects(10) /*
add10 called with 10
add10 called 1 times
20
*/
We've isolated the console.log
and variable mutation side effects to the edges of our code by defining them in their own functions. The final program is a composition of those side effecting functions and a pure function add10
. To be clear, add10WithSideEffects
is not pure; all we've done is move our side effects out to their own functions and, in a way, declare them with tap
. The goal here is not to be pure for purity's sake, but to have clean, readable code with organized side-effects.
-
logCalledWith
takes a number and logs 'add10 called with' number -
incNumCalls
takes nothing and increments the global variablenumCalls
-
logNumCalls
takes nothing and logs the global variablenumCalls
All of these functions are singly responsible for what they do. When used with pipe and tap in add10WithSideEffects
, the side effects of our program are clear.
Thanks for reading! You can find the rest of this series in the awesome resources section of rubico.
Photo credits:
https://www.pinterest.com/pin/213639576046186615/
Sources:
https://en.wikipedia.org/wiki/Pure_function
https://softwareengineering.stackexchange.com/questions/40297/what-is-a-side-effect
Posted on August 3, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.