DEV Community

Cover image for Practical Functional Programming in JavaScript - Side Effects and Purity
Richard Tong
Richard Tong

Posted on • Edited on

Practical Functional Programming in JavaScript - Side Effects and Purity

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
Enter fullscreen mode Exit fullscreen mode

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
*/
Enter fullscreen mode Exit fullscreen mode

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
*/
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
  • 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 like console.log. We'll use tap 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
*/
Enter fullscreen mode Exit fullscreen mode

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 variable numCalls
  • logNumCalls takes nothing and logs the global variable numCalls

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

Top comments (10)

Collapse
 
functional_js profile image
Functional Javascript

Nice work on the functional paradigm Richard,

Let me give you a couple ideas I use with piping and logging.

Let's say we have this...

const p1 = pipe(
  func1,
  func2,
  func3,
  func4,
);

//@tests
p1(val)

I keep log statements out of the funcs themselves, otherwise you can't run them in a loop.

One of the cognitively challenging things about pipes is keeping the output of one func compatible with the input of the next. So I'll use a set of strongly-typed log utils for dev-side troubleshooting.

Here's an example of logging a pipe (exaggerated with lots of log utils for demo purposes)

const p1 = pipe(
  func1, llArr,
  func2, lFirstElems(3),
  func3, lStr,
  func4, lNum,
);

//@tests
p1(val)

The "l" prefix means "log"
The "ll" prefix means "log length"
"Elems" implies an arr, so in this case only the first 3 elems (elements) are logged
Note that I keep the log util on the same line of the returning func, for readability.

If the type passed into the log util is wrong, it throws. (we have a bug!)
The throw message tells me what this "wrong" type was, and it's value (up to 500 chars).

By reading this, I know, for example, that func3 takes an arr and returns a str, and func4 takes a str and returns a num.

If not, this pipe won't run, it'll throw.

Collapse
 
richytong profile image
Richard Tong

Glad to hear from you functional, interesting idea for a strongly typed log util. What are you using for that right now?

Collapse
 
functional_js profile image
Functional Javascript

I'll do an article on logging and troubleshooting pipes later this week.
But here's an example of one of the log utils throwing on an unexpected type...

logging functional pipelines

Thread Thread
 
tech6hutch profile image
Hutch

This seems like a poor man's TypeScript.

Thread Thread
 
functional_js profile image
Functional Javascript

Typescript does not do runtime type checking.

Thread Thread
 
tech6hutch profile image
Hutch

Because I'm saying you should check it at compile time

Thread Thread
 
functional_js profile image
Functional Javascript

That won't help you with dynamic data at runtime.
That's where the runtime bugs are.
Thus this is outside the scope of Typescript.

Thread Thread
 
tech6hutch profile image
Hutch

Ok then I guess I misunderstood, sorry

Collapse
 
pentacular profile image
pentacular

A function's side effect is a modification of some kind of state beyond a function's control

It's not about being within control -- it's about reachability.

An effect doesn't exist for things which cannot be affected by it.

So in some cases we might consider logging output not to be a side-effect, if the log output is unreachable by that system.

Things like modifying local variables are fine, providing they do not escape, say by lexical closure.

They're fine because the changes to those variables cannot escape -- it makes the inside of the procedure clearly a procedure, but it doesn't affect the procedure's implementation of a pure function from the outside.

Collapse
 
richytong profile image
Richard Tong

Thank you! I think escape (e.g. via closure) is a good way to put it. I've amended my use of the word control to the word reach, for clarity.