Skip to content

Instantly share code, notes, and snippets.

@ericelliott
Last active February 2, 2023 23:33
Show Gist options
  • Save ericelliott/ea925c58410f0ae74aef to your computer and use it in GitHub Desktop.
Save ericelliott/ea925c58410f0ae74aef to your computer and use it in GitHub Desktop.
A Guide to Functional Programming Lingo for JavaScripters

A Guide to Functional Programming Lingo for JavaScripters

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). This sounds a lot scarier than it is. You can do this!

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.

Lets get some really scary words out of the way, first

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 ("Programming JavaScript Applications", O'Reilly), 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)); // 3

But 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)); // 3

D'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))); // 3

Whoah. What's this? add(wrap('1'), wrap('2')) Pretty simple, actually. We're taking the returned results from wrap('1') and wrap('2') and passing them into add() as arguments. This is called function composition. You've probably done it before. Now you know what to call it.

The only trouble is, that code 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)) ); // 3

Ah, 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.

What's a Functor?

What's a Monad?

What does Reactive Mean?

What is Lazy Evaluation?

@katychuang
Copy link

katychuang commented Jan 19, 2018

Thank you for writing this! 🙏

@LayZeeDK
Copy link

Change let wrap = (n) => return [n]; to let wrap = (n) => [n];.

@DawidLoubser
Copy link

DawidLoubser commented Jan 29, 2018

Another vote for fixing the syntax error. You can change

let wrap = (n) => return [n];

to

const wrap = n => [n]

Note: It's idomatic in FP-style ECMAScript to use const for function definitions (and anything non-mutable, really - as all things should be ;-) and to omit semicolons. People also often drop the brackets around single-argument functions.

@ryangreenberg
Copy link

Thanks for writing this guide—found it while looking for a plain explanation of lifting. (P.S. See http://grammarist.com/usage/bad-rap/ for bad wrap vs. bad rap)

@ericjgagnon
Copy link

@ericelliot this readme is super useful. That being said, I like to hear your explanations on the remaining topics, reactive and lazy evaluation

@ericelliott
Copy link
Author

This became a series of blog posts, which eventually became a whole book.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment