Skip to content

Instantly share code, notes, and snippets.

@ericelliott
Last active February 2, 2023 23:33
Show Gist options
  • Star 86 You must be signed in to star a gist
  • Fork 14 You must be signed in to fork a gist
  • 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:

const wrap = (n) => [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.

Let's 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:

const 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 with 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():

const 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'];

// Use Function.prototype.apply() to
// apply a function to an array of arguments.
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 args1 = ['1', '2'];
let args2 = [1, 2];

console.log( add.apply(null, args1.map(wrap)) ); // 3
console.log( add.apply(null, args2.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?

A functor is a wrapper that can apply a given function to its contents & return a new instance containing the results. e.g. JS Arrays.

What's a Monad?

Simple answer: Monads are chainable operations made with composable functions.

Complicated answer: I'm going to shamelessly steal this from a Stackoverflow answer that is the clearest description of monads I have ever seen:

OK, explaining "what is a monad" is a bit like saying "what is a number?" We use numbers all the time. But imagine you met someone who didn't know anything about numbers. How the heck would you explain what numbers are? And how would you even begin to describe why that might be useful?

What is a monad? The short answer: It's a specific way of chaining operations together.

In essence, you're writing execution steps and linking them together with the "bind function". (In Haskell, it's named >>=.) You can write the calls to the bind operator yourself, or you can use syntax sugar which makes the compiler insert those function calls for you. But either way, each step is separated by a call to this bind function.

So the bind function is like a semicolon; it separates the steps in a process. The bind function's job is to take the output from the previous step, and feed it into the next step.

That doesn't sound too hard, right? But there is more than one kind of monad. Why? How?

Well, the bind function can just take the result from one step, and feed it to the next step. But if that's "all" the monad does... that actually isn't very useful. And that's important to understand: Every useful monad does something else in addition to just being a monad. Every useful monad has a "special power", which makes it unique.

(A monad that does nothing special is called the "identity monad". Rather like the identity function, this sounds like an utterly pointless thing, yet turns out not to be... But that's another story™.)

Basically, each monad has its own implementation of the bind function. And you can write a bind function such that it does hoopy things between execution steps. For example:

If each step returns a success/failure indicator, you can have bind execute the next step only if the previous one succeeded. In this way, a failing step aborts the whole sequence "automatically", without any conditional testing from you. (The Failure Monad.)

Extending this idea, you can implement "exceptions". (The Error Monad or Exception Monad.) Because you're defining them yourself rather than it being a language feature, you can define how they work. (E.g., maybe you want to ignore the first two exceptions and only abort when a third exception is thrown.)

You can make each step return multiple results, and have the bind function loop over them, feeding each one into the next step for you. In this way, you don't have to keep writing loops all over the place when dealing with multiple results. The bind function "automatically" does all that for you. (The List Monad.)

As well as passing a "result" from one step to another, you can have the bind function pass extra data around as well. This data now doesn't show up in your source code, but you can still access it from anywhere, without having to manually pass it to every function. (The Reader Monad.)

You can make it so that the "extra data" can be replaced. This allows you to simulate destructive updates, without actually doing destructive updates. (The State Monad and its cousin the Writer Monad.)

Because you're only simulating destructive updates, you can trivially do things that would be impossible with real destructive updates. For example, you can undo the last update, or revert to an older version.

You can make a monad where calculations can be paused, so you can pause your program, go in and tinker with internal state data, and then resume it.

You can implement "continuations" as a monad. This allows you to break people's minds!

All of this and more is possible with monads. Of course, all of this is also perfectly possible without monads too. It's just drastically easier using monads.

http://stackoverflow.com/questions/44965/what-is-a-monad#10245311

What does Reactive Mean?

What is Lazy Evaluation?

@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