Preface
Goal: Exploring capability towards functional programming in Javascript.
Source Examples
You can obtain source examples here:
Javascript in Web development.
This blog is mainly talking about functional programming. For web development with javascript, you might desire to have a look at my other blog, for example this article series:
From Procedural to Functional
Functional programming capability in ecmacript is somehow limited.
Ecmascript is never been intended to be a full blown functional language.
And most of you doesn’t need pure functional anyway.
Ecmascript
is not Haskell
.
We all do code by how to do something, instead of what to do with it.
We have loops, conditional and so on.
However, functional approach might improve your coding a lot.
This article will show how you can get start functional programming,
even with limited support from ecmascript.
After years, Ecmascript
support,
for functional programming has getting better.
These days we can push our code to be more like functional approach,
rather than just imperative one.
Coding Style
For simplicity reason I separate functions into oneliner.
And in the same time, using a word for variable name,
instead of just x
, y
, a
or b
.
I believe, with this way, reader can understand easier.
Example in CLI
I also use CLI using nodejs
.
It is easier to get the result, compared with inspect element
.
And there is no need to use additional file such as html
or css
.
Just the script, and here we go.
1: Imperative Example (Procedural)
Example Data
Before you get confused, consider examining this real situation.
I use this real code for my eleventy
(11ty
) website.
I have a bunch of articles, with title and tags,
and I want to extract unique tags to be shown globaly,
in a page called Archive by Tags.
To make the example simpler, I have made ready to use data structure. Instead of articles, I use my favorite songs. The data can be shown as below:
const songs = [
{ title: "Cantaloupe Island", tags: ["60s", "jazz"] },
{ title: "Let it Be", tags: ["60s", "rock"] },
{ title: "Knockin' on Heaven's Door", tags: ["70s", "rock"] },
{ title: "Emotion", tags: ["70s", "pop"] },
{ title: "The River" }
];
Preparing package.json
We are going to walk these code with nodejs
.
In order to import data as module,
we have to set the type
as module
in package.json
.
{
"name": "songs-functional",
"version": "1.0.0",
"description": "",
"dependencies": {},
"author": "Epsiarto Rizqi Nurwijayadi",
"license": "ISC",
"type": "module"
}
Procedural Code
It is still working today.
In the old good ES6 (ecmascript 2015) days, a few years ago, we could write the code as below:
let tagSet = new Set();
// using set feature to collect tag names
songs.forEach(song => {
if( "tags" in song ) {
let tags = song.tags;
console.log(tags);
for (const tag of tags) {
tagSet.add(tag);
}
}
});
console.log(tagSet);
// normalize to array
let allTags = [...tagSet];
console.log(allTags);
With the result similar to figure below:
$ node 21-songs-es6.js
[ '60s', 'jazz' ]
[ '60s', 'rock' ]
[ '70s', 'rock' ]
[ '70s', 'pop' ]
Set(5) { '60s', 'jazz', 'rock', '70s', 'pop' }
[ '60s', 'jazz', 'rock', '70s', 'pop' ]
Functional Code
As a comparison, here is functional code using flatmap
feature,
from ES10 (Ecmascript 2019).
import songs from "./songs-data.js";
const unique = array => [... new Set(array)];
const allTags = unique(songs
.filter(song => song.tags)
.flatMap(song => song.tags)
);
console.log(allTags );
With the result similar to figure below:
$ node 14-songs-flatmap.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]
The thing is, writing functional code is easy. But the trade off is, this is not readable to beginner. Not because it is hard, but because they do not get used with the writing style.
Migration Issue
Swicthing Mind
The key to success is not about coding skills. But rather about switching mind from imperative thinking, to functional thinking.
You can spot mistakes, people doing functional programming, using imperative thinking in below code:
import songs from "./songs-data.js";
// using map without returning value is considered anti pattern.
const addTags = (tagSet, tags) => tags.map(tag => tagSet.add(tag));
const addSongTags = (tagSet, song) =>
song.tags? addTags(tagSet, song.tags) : undefined;
const addSongsTags = (tagSet, songs) =>
songs.map(song => addSongTags(tagSet, song));
const setToArray = tagSet => [...tagSet];
// using set feature to collect tag names
let tagSet = new Set();
addSongsTags(tagSet, songs); // no assignment
let allTags = setToArray(tagSet);
console.log(allTags);
The code is not just difficult to be understood. But most of all, the code is just, an imperative thinking in functional fashioned.
2: Custom Oneliner: Unique (Distinct)
The way javascript solving unique
array has evolved from time to time.
Consider simple data as below:
const song = { title: "Cantaloupe Island", tags: ["60s", "jazz"] };
With ES 2015, we can solve unique array using Set
.
And with ES 2018, we can get the Set
back to array easily,
using spread operator as below code:
const tags = ["rock", "jazz", "rock", "pop", "pop"];
// using set feature to collect tag names
let tagSet = new Set();
for (const tag of tags) {
tagSet.add(tag);
}
console.log(tagSet);
// normalize to array
const allTags = [...tagSet];
console.log(allTags);
With ES 2015, we can rewrite code above using fat arrow ,
also allowing array as direct parameter of Set
class.
const unique = array => [... new Set(array)];
const tags = ["rock", "jazz", "rock", "pop", "pop"];
const allTags = unique(tags);
console.log(allTags);
With result as below:
$ node 03-tags-unique.js
[ 'rock', 'jazz', 'pop' ]
And you can also examine,
how this custom Set class
behave,
by testing on empty tags:
const unique = array => [... new Set(array)];
const song1 = {
title: "Cantaloupe Island",
tags: ["rock", "jazz", "rock", "pop", "pop"]
};
const song2 = { title: "The River" };
const song1Tags = unique(song1.tags);
console.log(song1Tags);
const song2Tags = unique(song2.tags);
console.log(song2Tags);
The result should be as below:
$ node 04-song-empty.js
[ 'rock', 'jazz', 'pop' ]
[]
3: Flat and Flatmap
I should write this style FP more often 🙂, in my project.
With ES 2019 flat
and flatmap
feature.
There are new ways to extract tags
from songs
data.
You may spot that all code has const
, instead of let
.
This means all variablesd are immutable.
If you find out let
cluase in your code,
it means your code has side effect.
Thus not truly following functional paradigm.
Using Flat
import songs from "./songs-data.js";
const
unique = array => [... new Set(array)],
exist = array => array.filter(a => a !== undefined),
tagsOnly = songs => songs.map(song => song.tags);
const allTags = unique(exist(tagsOnly(songs).flat())) ;
console.log(allTags);
With result as below:
$ node 11-songs-flat.js
[ '60s', 'jazz', 'rock', '70s', 'pop' ]
We make custom exist
function, as additional helper function.
Using Flatmap (plain)
We can combine flat
and map
as just one flatmap
function.
const
unique = array => [... new Set(array)],
exist = array => array.filter(a => a !== undefined);
const allTags = unique(exist(songs.flatMap(song => song.tags)));
With about the same result as above.
Using Flatmap (with inner ternary)
const unique = array => [... new Set(array)];
const allTags = unique(songs.flatMap(song => song.tags? song.tags: []))
With about the same result as above.
If you don’t mind being cryptic you can write them all as oneliner.
import songs from "./songs-data.js";
console.log([... new Set(songs.flatMap(song => song.tags || []))]);
Just remember that this is not the best practice. Clarity first, or you might end up getting dizzy the other day.
Using Flatmap (chained with filter)
The syntax in example below is concise. Brief, but comprehensive.
const unique = array => [... new Set(array)];
const allTags = unique(songs
.filter(song => song.tags)
.flatMap(song => song.tags)
);
Still with about the same result as above.
Using Flatmap (chained with oldschool unique filter)
const allTags = songs
.filter(song => song.tags)
.flatMap(song => song.tags)
.filter((v, i, a) => a.indexOf(v) == i)
;
console.log(allTags );
With exactly the same result as usual.
I know the last line is cryptic.
But it is the only chaining way I know.
Using standard filter
,
without messing with array prototype,
by making custom global chain property.
Unique Using Recursive Function
But why reinvent the wheel?
However we can solve unique elements with simple recursive function.
const unique = tags => {
if (tags.length <= 1) {
return tags
} else {
const [head, ...tail] = tags
const newTails =
unique(tail.filter(tag => tag != head))
return [head, ...newTails]
}
}
const tags =
["rock", "jazz", "rock", "pop", "pop"]
console.log(unique(tags))
With result as below:
$ node 31-tags-unique.js
[ 'rock', 'jazz', 'pop' ]
What is Next 🤔?
We are not finished yet. I will give an example of bad code.
Consider continue reading [ Ecmascript - Towards Functional - Part Two ].