Functional programming is an awesome tool to have in your arsenal, since it will at least: Reduce the amount of mutations; Make the code base easier to test; Composition and changes are made easier; You can apply it right now in your code base since it's just a programming paradigm (a set of rules).
I will go for the approach of showing you the imperative way, point out what parts of the code aren't imperative and finishing with the functional example. I won't be going into monads, functors and all that jargon, the objective of this article it' to help you identify weak links in your code and how to make them hopefully better with functional programming. If that interests you keep reading ^^
Mutation - Whenever a variable that was already set is changed that change is called a mutation, you are changing the original value of it. These should be avoided in functional programming, one recommendation is to use const
most of the time and for the occasions you have no other choice then use let
but forget about var
, it is mostly deprecated by now in my view;
Pure functions - When a function doesn't have “outside influence” it is called a pure function. So if you want some value inside of a function you send it by parameters, the function doesn't use anything outside its scope;
Map - map
is used to iterate array lists and it will iterate over all of the array, you can make changes for each iteration in its callback and what you return will be in an array with the new values at the end:
const result = [1,2.3].map((value) => {
return value + 1
})
console.log(result) //[2,3,4]
Filter - filter
it's just like map
only that is used when you want to make an array smaller:
var result = [1,2,3].filter((value) => {
return value > 2
})
console.log(result) // [3]
Reduce - reduce
is quite useful when you want to transform lets say, an array into an object (basically changing types) or for when neither map
or filter
can be applied for whatever you are trying to do. It will accumulate the data you return in the callback and when the iteration is finished it returns the result, something like:
const result = ['zero', 'one', 'two']
.reduce((accumulated, current, index) => {
return Object.assign({}, accumulated, {
[current]: index
})
})
console.log(result) // {zero: 0, one: 1, two: 2}
Recursive functions - For functions that call themselves to do iterations we call them recursive, they can be confusing to create and to read but are the alternative to for
and while
loops for when map
, filter
and reduce
are to convoluted for you implementation. Reworking the filter
“larger than two” you can end up with something like:
function largerThanTwo(values, index = 0, result = []) {
if (!values[index]) return result
if (values[index] > 2) {
result.push(values[index])
}
return largerThanTwo(values, (index+1), result)
}
const result = largerThanTwo([1,2,3])
console.log('--->', result)
I will take a gamble and say that most JavaScript programmers are programming in the imperative paradigm since it's the predominant one in tutorials and manuals (while it seems to be changing due to the big push for functional).
While there isn't anything “wrong” per say with imperative code it does end up like a castle of cards eventually, you will have an unmovable base that you cannot change without crumbling the stacked cards.
Take this example:
Let us say that we have a list of artists but for reasons out of our control each artist best photo and camera used are in other two separate lists and we want to have everything in just one, unique list. So the data would be something like this:
const photos = ['photoMoon', 'photoSun', 'photoSky']
const cameras = ['canon', 'nikon', 'Sony']
const artists = [{
name: 'Thomas Kelley',
link: 'https://unsplash.com/@thkelley',
}, {
name: 'Thomas Kvistholt',
link: 'https://unsplash.com/@freeche',
}, {
name: 'Guillaume Flandre',
link: 'https://unsplash.com/@gflandre',
}]
Our expected result is:
[{
name: “Thomas Kelley”,
link: “https://unsplash.com/@thkelley",
photo: “photoMoon”,
camera: “canon”
}, {
name: “Thomas Kvistholt”,
link: “https://unsplash.com/@freeche",
photo: “photoSun”,
camera: “nikon”
}, {
name: “Guillaume Flandre”,
link: “https://unsplash.com/@gflandre",
photo: “photoSky”,
camera: “Sony”
}]
This should be easy enough, just a loop and were done:
const photos = ['photoMoon', 'photoSun', 'photoSky']
const cameras = ['canon', 'nikon', 'Sony']
// can be const since it's an array will keep as let to identify that it's being changed
let artists = [{
name: 'Thomas Kelley',
link: 'https://unsplash.com/@thkelley',
}, {
name: 'Thomas Kvistholt',
link: 'https://unsplash.com/@freeche',
}, {
name: 'Guillaume Flandre',
link: 'https://unsplash.com/@gflandre',
}]
function artistsCorrect() {
for (let i=0; i<artists.length; i++) {
artists[i].photo = photos[i];
artists[i].camera = cameras[i];
}
}
artistsCorrect()
console.log('result ->', artists)
There are three rules to make our code functional that we are going to follow:
No variables - This means that no mutation can happen to our variables, once declared they should never be changed;
No for
and while
loops - Instead of using these that usually make your create some sort of auxiliar function map
, filter
, reduce
or a recursive function (one that calls itself) should be used instead (foreach
and the likes also break the rule);
At least utility functions (those that aren’t used to change the DOM or as entry points to your code) should be pure
.
So given our code above we can identify that we are breaking those rules by using a for
loop and we have two mutations in the form of
artists[i].photo = photos[i];
artists[i].camera = cameras[i];
We are changing the original value of the artists
variable which breaks the “No variables” rule.
We have a impure function so let us start with that, purity has stated in the Jargon section of this article is having a function that doesn't matter when in the program it is called given a certain input it will return the same output.
Right now if we continue to code and change the artists again, when the artistsCorrect
function is called again it will have a different functionality which we want to avoid.
We rewrite like so:
function artistsCorrect(artistsList, photosList, camerasList) {
for (let i=0; i<artistsList.length; i++) {
artistsList[i].photo = photosList[i];
artistsList[i].camera = camerasList[i];
}
return artistsList
}
const result = artistsCorrect(artists, photos, cameras)
console.log('result ->', result)
Our function will only use data supplied to it by the arguments, accessing nothing from the outer scope so if we call it in line 20 of our code or in line 400 if the input is the same in both scenarios the output will to. Great!
Now we have to get rid of for
and the mutations, I can tell you right now that it is done in one fell swoop by replacing the for
with map
like so:
function artistsCorrect(artistsList, photosList, camerasList) {
return artistsList.map((artist, index) => {
const extraValues = {
photo: photosList[index],
camera: camerasList[index]
}
return Object.assign({}, artist, extraValues)
})
}
const result = artistsCorrect(artists, photos, cameras)
console.log('result ->', result)
Well, that is a bit more changes than expected let us go into it:
map
iterates just like a for
each value and at the end it creates a new array with the changes made on each index, this keeps the original array untouched which makes our mutation disappear;
Object.assign is used to create two or more objects into a new one without having to deal with reference (for at least the first level of the object). So it will create a new object using artist
and extraValues
. If less confusing and for easier learning purposes you could replace that with:
return {
name: artist[index].name,
link: artist[index].link,
photo: photosList[index],
camera: camerasList[index],
}
The downside to that is that if you add a new property you must remember to add it here which is not something I would recommend since it is easy to forget and obscure for someone new to the codebase. Even mutability is preferred in this scenario since it's self contained but again, would not recommend for your “finished code”. Nonetheless I find it better to explain this little detail instead of just having you blindly trust me.
“But wait a minute you just said I should do pure functions and that callback is using photoList
and camerasList
- Some of you might point this out so let me give you my input on that. While it is true that the anonymous function used in this case on map
it's not pure it is a self contained function inside artistsCorrect
that is not actually accessed by the rest of your program so you won't have the downsides of impurity. In other words, you know that no matter what you change outside artistsCorrect
it won't have any impact on that impure function since it's using nothing from there only the arguments that are passed thought.
You can also check a different approach done by Dan Wilson reply here with ES6+ features.
The code its a bit more verbose than the imperative way but now we always have access to our initial values since we didn't change artists
and our artistsCorrect
will always return the same thing given the same input no matter what you do to your outer code.
const photos = ['photoMoon', 'photoSun', 'photoSky']
const cameras = ['canon', 'nikon', 'Sony']
const artists = [{
name: 'Thomas Kelley',
link: 'https://unsplash.com/@thkelley',
}, {
name: 'Thomas Kvistholt',
link: 'https://unsplash.com/@freeche',
}, {
name: 'Guillaume Flandre',
link: 'https://unsplash.com/@gflandre',
}]
function artistsCorrect(artistsList, photosList, camerasList) {
return artistsList.map((artist, index) => {
const extraValues = {
photo: photosList[index],
camera: camerasList[index]
}
return Object.assign({}, artist, extraValues)
})
}
const result = artistsCorrect(artists, photos, cameras)
console.log('result ->', result)
To someone not used to functional this might seem like an “overkill” rework, what I can assure you is that, by experience, once the code base grows and you have not just one function but dozens or hundreds of functions you will thank yourself for the fact that they are independent from the outer code since the less outside global variables you have to worry in a function the faster it is to handle some sort of problem with a function, and easier to test!
The fact that you can always count on your original values since you never mutate its something that will also help you while debugging, you end up with way less “where the hell is this variable being changed” moments.
Using something that is unfamiliar can be overwhelming and weird at first that is why I tried to keep things simple, what I can recommend is for you to continue to do your code normally but before you push it to the server have another look at it and try to identity at least one of the three rules that is being broken and change the code accordingly.
It is time consuming and you might not see any advantage at first but once you get the hang of it and your code testing is made easier you will, maybe without noticing, start to apply those rules as a standard.
While I do enjoy a lot the functional approach to things it should be noted that JavaScript is not fully functional, specially when on front end since you end up having to change the DOM and that on itself is a mutation (hence not functional) so not everything can be changed to functional, just keep the mutations in entry points that can be easily be identified.
When using libraries like for example React with Redux they “push” you to avoid touching the DOM directly and give you a store for data, in this cases applying functional without imperative tends to be easier on the front end.
As a rule of thumb just remember, while it would be great to have all the code functional sometimes the functional solution ends up being hard to read for you and your colleagues, the same happens with some imperative code so, and hopefully I also was able to do it in the article, follow the straightforward rule of...
KISS! - Keep It Simple Stupid!
If you're interested in working with Functional JavaScript, check out our JavaScript Works job-board here.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!