Functional programming gets a bad wrap about being too hard for mere mortals to comprehend. This is nonsense. The concepts are actually quite simple to grasp.
The jargon is the hardest part. A lot of that vocabulary comes from a specialized field of mathematical study called category theory (with a liberal sprinkling of type theory and abstract algebra).
All examples using ES6 syntax. wrap (foo) => bar means:
function wrap (foo) {
bar = [foo];
return bar;
}In a nutshell, functions have types like f (a) -> b which means f is a function which takes type a and returns type b. Here's an example that should look familiar:
let wrap = (n) => return [n];This example takes a single value and wraps it with an array. Why is that useful? Because it turns out there are a ton of abstractions that can work on any type because they're really about dealing with lists rather than dealing with individual values. All such abstractions can be lifted such that they work on inputs of any type.
Polymorphism, homomorphism, monomorphism, what is a @@!$$ morphism?
Category theory has fancy words for everything. Everywhere you see "object" in category theory text, think "type" (I know, confusing for computer programmers, huh?) and every time you see "morphism" think "function".
pause for mind explosion
Yeah. That explains a lot, right?
As you should know if you read my book, a polymorphic function is a function that can take and/or return multiple types. There are two types of polymorphism you'll commonly encounter in JavaScript: ad-hoc polymorphism (avoid this one if you can), and parametric polymorphism.
What's the difference? With ad-hoc polymorphism, you tend to write code like this:
let add = (a,b) => {
if (typeof a === 'string' || typeof b === 'string') {
return Number(a) + Number(b);
} else if (typeof a === 'number' && typeof b === 'number') {
return a + b;
}
};
console.log(add('1', '2')); // 3
console.log(add(1, 2)); // 3But that's a bit silly, isn't it? What if you could always count on the input types to do arithmatic addition the + operator? One way to guarantee that is to make sure that all inputs support it. In this case, support for arithmatic addition is a requirement.
Writing a function to work for any input that supports a specific set of requirements is called lifting. Another way to think of lifting is that you abstract away the differences between the concrete implementations of a function.
Let's lift add():
let add = (a,b) => {
return a + b;
};Much simpler, right? But now we have a problem:
console.log(add('1', '2')); // 12
console.log(add(1, 2)); // 3D'oh! Now what? Well, let's just make sure that everything we send in gets converted, first. Let's spin off that wrap function above:
let wrap = (n) => Number(n);Now we can do this:
console.log(add(wrap('1'), wrap('2'))); // 3
console.log(add(wrap(1), wrap(2))); // 3But that seems even worse than the ad-hoc version of add. Unless there's a neat trick up my sleeve...
let args = ['1', '2'];
add.apply(null, args.map(wrap));So now the full source is:
let add = (a,b) => {
return a + b;
};
let wrap = (n) => Number(n);
let args = ['1', '2'];
console.log( add.apply(null, args.map(wrap)) ); // 3
console.log( add.apply(null, args.map(wrap)) ); // 3Ah, that's better, but if you're paying really close attention, maybe something is starting to click. This is still a little awkward, but there's a light at the end of the tunnel. Time to abandon this example and dig a little deeper.
Thank you for writing this! 🙏