After a few years, I'm re-reading JavaScript Allongé, the first book that made functional programming click for me.
It starts off very basic... the first time I read it I thought it was just going to be another "Intro to JavaScript" book. But no! It quickly dove deep into some concepts that were foreign to me at the time (currying, combinators).
I like to revisit books and articles after a while, especially the ones I didn't fully understand, or helped me get a concept for the first time. Even in some basic material there are usually a few good nuggets.
Last night as I was re-reading this chapter, I realized I was missing something about how recursion works in JavaScript. I use recursion all the time but somehow never gave much thought to how a function calling itself actually works.
There are a couple ways recursive functions can call themselves. One way is as an anonymous function assigned to a variable:
const isEven = num => {
return num === 0 || !isEven(num - 1);
};
when they “call themselves,” what they actually do is look themselves up by name in their enclosing environment.
This is obvious to me in retrospect. The function looks for something called
isEven
in the local scope, and when it doesn't find it moves to the parent
scope. But I was surprised that this felt like new information.
The other way they work: as a pure function using a named function expression:
const isEven = function isEven(num) {
return num === 0 || !isEven(num - 1);
};
The subtle difference here is the function is named isEven
, not just
assigned to a variable named isEven
. Why does this matter?
This is different, because the function doesn’t refer to a name bound in its enclosing environment, it refers to a name bound in its own body.
It's name bound in its own body! This is important, because you can reassign
isEven
in the enclosing scope and the recursion still works, which isn't true
in the first example (see the chapter for an explanation). Granted, I would be
surprised to see this type of definition/reassignment done in practice, but it
reveals something about functions and scope.
I don't know why this was such a revelation, but it felt like a real aha moment.
I like to share things like this for a few reasons. It might help someone else solidify their understanding of a familiar concept, but also it makes me a little uncomfortable to share things I feel I "should know already." It's a reminder that everyone has gaps. By being open about my blind spots I hope I can encourage others to do the same, and we'll all gain from it.
On the other end of the spectrum, one thing I've always struggled to fully understand is the Y combinator (the acual combinator, not the incubator and home of Hacker News). I've always had a hard time understanding what the Y combinator is, what it does, and why it's useful. From Wikipedia:
In functional programming, the Y combinator can be used to formally define recursive functions in a programming language that does not support recursion.
I've never worked in a language that doesn't support recursion, so I can't quite wrap my head around what that would actually look like. Here's what the Y combinator itself actually looks like:
const Y = (g) => {
return ((f) => {
return f(f);
})((f) => {
return g((n) => {
return f(f)(n);
});
});
};
That's bananas.
I first read this article a couple years ago and it was far, far over my head. But I recommend reading things you don't fully understand. You'll absorb more than you realize and it's all adding to your mental model. Later on, your brain will connect it to something and it will click into place.
It's not like there's a reason I need to understand it, but the fact that it's just beyond my grasp makes me want to reach for it.
I read it again on a trip to Providence, and I got a lot more out of it. But I'm not ashamed to admit I still don't understand it well enough to explain it to someone.
Maybe it's time for another read!