This post start a short series where we discuss functional programming. We'll discuss what functional approach is, what functional pipes and monads are.
Functional programming is about functions. In an ideal world, functions are pure means they have no side effects. Also, variables should be immutable which means that once variable value is set, it's never modified. This effectively means simple loops (for, while, do) are replaced with recursion.
Why? Well, often, the code is easier to read and reason about.
Before we dig into more complicated examples, let's start with discussing the difference between imperative and functional code. Suppose we need to map an array through a function.
A naive imperative approach would be:
function map( xs, f ) {
var ret = [];
for ( var i=0; i<xs.length; i++ ) {
ret.push( f(xs[i]) );
}
return ret;
}
var a = [1,2,3,4,5];
console.log( map( a, _ => _**2 ) )
There's nothing wrong with the code. Note however, some subtle drawbacks. First, the loop index must be carefully controlled. Also, both the index and the return value accumulator are mutable. Under usual circumstances, this is safe, however, in a complicated, multithread code, mutable variables can be a pain if are used without a care.
A functional version of this will still have two parameters but there should be no loop but a recursion. In JavaScript, we can have an auxiliary function hidden inside the original one.
Also, we can show few possible approaches.
Let's start with a common technique, the Accumulator Passing Style. This approach is often the easiest one to understand by imperative programmers. It consists in passing an extra parameter between recursive calls, the parameter that accumulates the return value between recursive calls. In our map function, we start with empty array and then we add a new mapped value in each recursive call:
function map( xs, f ) {
return (function rec(i, acc) {
if ( i<xs.length ) {
return rec(i+1, acc.concat(f(xs[i])));
} else {
return acc;
}
})(0, []);
}
Easy. The index is still there, the accumulator is the extra parameter, the recursion seems rather straightforward.
Next approach demonstrates a common technique in which there's no explicit accumulator. Instead, the return value of the inner function is used to accumulate the return value of the outer function:
function map( xs, f ) {
return (function rec(i) {
if ( i<xs.length ) {
return [f(xs[i])].concat( rec(i+1) );
} else {
return [];
}
})(0);
}
Please take time to carefully study the difference. Debug if you need.
Next approach is a step forward into the functional world. Instead of passing the index in recursive calls (starting from 0), we will pass the array as a list and in each iteration we'll split the array (the list) into the head and the tail. JavaScript's array mimicks a list, the head (the first element) is just a[0] and the tail is just a.slice(1):
function map( xs, f ) {
return (function rec(ys) {
if ( ys.length >= 1 ) {
return [f(ys[0])].concat( rec(ys.slice(1)) );
} else {
return [];
}
})(xs);
}
We are almost there. The next refactoring splits the array (the list) into the head and tail in an explicit way, using the possibility to destructurize function parameters. Note also how the spread operator is used to obtain the tail:
function map( xs, f ) {
return (function rec([head, ...tail]) {
if ( head ) {
return [f(head)].concat( rec(tail) );
} else {
return [];
}
})(xs);
}
Technically of course, the if ( head ) is wrong because of null coercions, correct it to if ( head !== undefined ) if you need.
It can be further refactored to be slightly less verbose:
const map = ( xs, f ) =>
(function rec([head, ...tail]) {
return head
? [f(head)].concat( rec(tail) )
: []
})(xs);
Let's stop there. Go back to the very top and compare both functions, the first imperative one and the last functional one. It's still the same language but two different programming styles.
That's why we say such languages are hybrid - if both paradigms, the imperative (or even object-oriented) and functional styles are possible and feel natural in the language.