Edit: This article doesn't do such a great job at communicating what I originally intended, so it has a revision. I recommend you read the revised version, though I've left this original for historical purposes.
Hello 🌍. You've arrived at the nth installment of my series on functional programming: Practical Functional Programming in JavaScript. On this fine day I will talk about a two-pronged approach to problem solving that makes life easy: Side Effects and Purity.
Let's talk about purity. A function is said to be pure if it has the following properties:
- Its return value is the same for the same arguments
- Its evaluation has no side effects (source)
Here's side effect from stackoverflow:
A side effect refers simply to the modification of some kind of state - 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
Basically, all interactions of your function with the world outside its scope are side effects. You have likely been using side effects this whole time. Even the first "hello world" you logged out to the console is a side effect.
In a world full of side effects, your goal as a functional programmer should be to isolate those side effects to the boundaries of your program. Purity comes into play when you've isolated the side effects. At its core, purity is concerned with data flow, as in how your data transforms from process to process. This is in contrast to side effects, which are only concerned with doing external stuff. The structure of your code changes for the clearer when you separate your programming concerns by side effects and purity.
Here is an impure function add10
:
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. The console logs are side effects because they're logging out to the console, which exists in the world outside add10
. Incrementing numCalls
is also a side effect because it refers to a variable in the same script but outside the scope of add10
. add10
is not pure.
By taking out the console logs and the variable mutation, we can have a pure add10
.
let numCalls = 0
const add10 = number => number + 10
console.log('add10 called with', 10) // > add10 called with 10
numCalls += 1
console.log('add10 called', numCalls, 'times') // > add10 called 1 times
add10(10) // > 20
Ah, sweet purity. Now add10
is pure, but our side effects are all a mess. We'll need the help of some higher order functional programming functions if we want to clean this up.
You can find these functions in functional programming libraries like rubico (authored by yours truly), Ramda, or RxJS. If you don't want to use a library, you can implement your own versions of these functions in vanilla JavaScript. For example, you could implement minimal versions of the functions we'll be using, pipe
and tap
, like this
const pipe = functions => x => {
let y = x
for (const f of functions) y = f(y)
return y
}
const tap = f => x => { f(x); return x }
We'll use them to make it easy to think about side effects and purity.
-
pipe takes an array of functions and chains them all together, calling the next function with the previous function's output. Since
pipe
creates a flow of data in this way, we can use it to think about purity. You can find a runnable example in pipe's documentation. -
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". Super useful for side effects. You can find a runnable example in tap's documentation.
Here's a refactor of the first example for purity while accounting for side effects using pipe
and tap
. If the example is looking a bit foreign, see my last article on data last.
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
pipe([
tap(logCalledWith), // > add10 called with 10
tap(incNumCalls),
tap(logNumCalls), // > add10 called 1 times
add10,
])(10) // > 20
We've isolated the console log and variable mutation side effects to the boundaries of our program by defining them in their own functions logCalledWith
, incNumCalls
, and logNumCalls
. We've also kept our pure add10
function from before. The final program is a composition of side effecting functions and a pure function, with clear separation of concerns. With pipe
, we can see the flow of data. With tap
, we designate and isolate our side effects. That's organized.
Life is easy when you approach problems through side effects and purity. I'll leave you today with a rule of thumb: if you need to console log, use tap.
Next time, I'll dive deeper into data transformation with map
, filter
, and reduce
. Thanks for reading! You can find the rest of the series on rubico's awesome resources. See you next time for Practical Functional Programming in JavaScript - Intro to Transformation
Top comments (12)
That has to be the clearest description of
tap()
I've seen. Thanks!Your program is still not pure.
That's not the way you handle side effects in functional programs, you should never have impure functions.
The simplest way to make that program pure is by having incNumCalls return the new state, having the logNumCalls be lazy, meaning functions that return the effect of logging, but don't log.
Logging in functional programs is hard to implement and generally leverages the Writer monad, which, differently from your example, truly keeps the effect outside the boundary.
kseo.github.io/posts/2017-01-21-wr...
Says who? Why am I never allowed to have impure functions?
This doesn't sound very simple to me. I don't see how returning an effect of logging but not actually logging makes my life any easier. Is it not simpler to just log when you want to log?
Logging in functional JavaScript programs is easy if you don't leverage the Writer monad. Maybe in Haskell the definitive way is to use a Writer monad, but in JavaScript you can just console.log. It's quite straightforward.
I never said my program is pure. The final
add10
is pure, and the other functions are effectful. I said "the final program is a composition of side effecting functions and a pure function". I was never trying to make my entire program pure in the first place. You can't just make your side effects go away. At some point you have to deal with them. Why not deal with them at the boundaries of the same program?It seems to me you are prescribing the practices you picked up in Haskell onto JavaScript. That is not sensible. Different languages have different boundaries for effects. In Haskell, you straight up declare your effects with the types, so it seems like the rest of your program is pure (which it is, in Haskell). In JavaScript, the effects are implied in the syntax and global functions, for example, console.log very specifically has the effect of writing out to the console.
I don't see how this belongs here. I don't need Monads to do what I need to do. I especially don't need them to program functionally in JavaScript. In fact, I started my library rubico to double down on functional composition and rebuke the complex hierarchy of effectful types of "traditional" functional programming as prescribed by zealots such as yourself.
Well, you can do what you want, but you shouldn't be calling it functional programming, as it's not really.
Lazy Pure Static Functional Programming isn't "functional programming". It's a weird corner of FP introduced in the 80s that has few inherent advantages.
Almost all the ideas Haskell introduces are to solve problems Haskell creates, ie., those due to lazyness.
In a language with a semi-colon you do not need a monad.
"Functional Programming" as in Programming with Functions can only be pure because a non-pure function is simply NOT a function (it is a procedure). It might be argued that the lazyness is optional, and in fact it is but as soon as you write a side effect inside one of your "functions" and that gets executed you will transform by definition your program to be procedural given the composition of a function with a procedure is a procedure.
I would just like to point out this interesting take from the MDN web docs on functions. (link)
It sounds like they're saying a function and a procedure are the same. Is there a misunderstanding here?
There is a misunderstanding, the definition of function is a binary mapping between sets. The interpretation from MDN is strictly related to the world of programming languages where the term function is improperly used to represent any procedure. It is not the meaning used in "functional programming" where functional refers to a classic function. Sometimes in programming we like to add the term "pure" to denote when a function (from the language perspective) is actually a function.
One thing to note is that all of the nice properties of "pure" functions that are leveraged in FP are almost never respected when functions are not "pure" (or better, not functions)
Inspired by this article, I fiddled around to enable a basic functional pipeline in js-coroutines so I could have a runner that started generator functions that
yield
to do a time check and enable its processing over multiple frames functionality. Wondering though if it would be better off making something for rubico that would do that same as my knowledge is definitely lacking in this area.60fps with Functional Programming in idle time
Mike Talbot ・ Jul 3 ・ 3 min read
I've mulled over supporting generator functions in rubico before, but couldn't find a great use case from my experience. I see your example with a generator function used with async functions here
I see how this is useful, but I'm trying to think of all the use cases. The
data
andoutput
make sense to me - data goes in and output goes out. I seeyield
in this case freeing up the event loop and splitting the processing up over multiple frames (assumingpipe
is fully consuming the generated iterable then returning the output). Are there any other use cases foryield
in the context of a generator function inpipe
?I'm a little hesitant to add in generator functions if this is the only use case (which I feel like it's not, I just need help with use cases) because I think I can achieve the same process splitting with async/await.
The primary function of yield in js-coroutines is to test the amount of time remaining in the current idle part of a frame. You may also yield promises to await their completion.
js-coroutines is just basically providing a collaborative multi-tasking program runner. It does this by exposing everything it touches as an
async
function - so actually heavy lifting can be done in any pipe that can await async functions. That's enoughStand up clap....THANK you for the fantastic article.