Functional Legos
When I first got involved in programming in the early 80's, the language we learned was MS-BASIC. Good language, great way to learn, very procedural.
What do I mean by procedural? Its a particular coding style, involving a series of steps:
1. do this thing
2. do that thing
3. do a loop
a. do this inside the loop
b. then do that inside the loop
4. end the loop
5. continue doing things
6. until the program ends
The above "pseudo-code" demonstrates how procedural code might work. It is very imperative, in that you are telling the interpreter or compiler exactly what to do, and how to do it.
A more modern approach becomes a little more declarative. The language engine is still told what do do, but with far less emphasis on how to do it. This might be Event-driven or Object-oriented programming, where an event or trigger can occur at any time, and the code is prepared to catch and handle that.
Functional programming is also declarative, in that I am creating or consuming functions that describe what is to be done, rather than stressing the how of it. And functional programming is what I wanted to write this about.
Functional Building Blocks
To begin this discussion, let's create some very basic functions. They may seem useless, and trivial, and they sort of are. The point is not how all-encompassing our functions are, but rather:
- are they self-descriptive?
- are they as atomic as possible?
- are they consistent, returning the same results given the same input?
- are they side-effect-free, to the greatest extent possible?
Let's get some code, so we can see what all that means.
const isEqual = function(comparator){
return function(value){
return comparator === value
}
}
const isGreaterThan = function(comparator){
return function(value){
return value > comparator
}
}
// or the same thing in ES6 "fat-arrows"
const isEqual = (comparator) => (value) => value === comparator
const isGreaterThan = (comparator) => (value) => value > comparator
Now these are simple functions, and yes, they have a bit of code to them. It would be quicker to simply write:
// this is shorter...
const lowerLimit = 18
if( myAge > lowerLimit) { ...}
// but this is pretty descriptive
const lowerLimit = 18
const isOfLegalMajority = isGreaterThan(lowerLimit)
if( isOfLegalMajority(myAge) ) {...}
But again, the point isn't to keep it short. The point is twofold:
- make it descriptive,
- and make it modular
Our functions, above, will let us build on top of them. We can use them elsewhere, and we can combine them in fun ways. By making them atomic, we can build code "molecules" from them. Of course, we'd need more pieces...
// This simply inverts a value from truthy to falsy.
const isNot = (value) => !value
// Lets use that one and our other functions!
const isNotEqual = (comparator) => (value) => isNot( isEqual(comparator)(value) )
const isLessThan = (comparator) => (value) => isNot( isGreaterThan(comparator)(value)
Congratulations! We've written functions to abstract away ===
, !==
, <
and >
! Useful, sure. Overly lengthy and sort of pointless... maybe. But let's press on, we'll get somewhere soon.
Composable Functions
That last bit we did, taking the output from our isEqual
or isGreaterThan
and using that as the input for our isNot
function, is the beginning of a powerful concept: composition.
Our functions thus far haven't been very powerful, or imaginative. Think of them as a 1x1 Lego block. By itself, not much. But when we start using them with other parts, combining them into something larger, then they start to shine.
Composing functions is just that - building larger and larger structures from our atomic, single-purpose ones.
In order to do much more, though, we should create two more atomic functions. While the ones we've written so far do one thing (think of them as a 1x1 Lego), we might want to be able to combine multiple functions, and get a single output from them.
Note that I'm really simplifying here. Our functions in this exercise are simply returning a true/false
value. I'm doing this deliberately, as I now plan to write two functions to combine multiple true/false
values into a single output.
We're going to write a function that takes an array of functions that act on some value, and return true
or false
depending on those actions. Our function then returns to us another function, which will take the value on which all of those functions should be run.
and(arrayOfConditions)(value)
The first one, and()
, returns a single function that requires that every one of its array must return true for that given value. Let's see how that might look:
const and = function(arrayOfConditions){
/***
* Here, we return a function that expects some value.
***/
function(value){
/***
* We'll take that value, and pass it to each of the conditions
* in turn. From each, we expect a truthy response. If we don't
* get that, we return false.
***/
return arrayOfConditions.every( function(condition){
// And in here, we check if this condition returns true or false.
return condition(value)
}
}
}
// The same thing in ES6:
const and = (arrayOfConditions) => (value) => arrayOfConditions.every( (condition) => condition(value) )
That function will let us combine (or compose) any number of functions. So long as they all return true
(or an appropriate truthy value), our containing function also returns true
. This is simply a long way of writing the &&
logical operator.
or(arrayOfConditions)(value)
The second function, or
, does much the same as and
- but the key difference is that if any of the functions in the array return true
, we return true
for the entire thing. Think of this as a wordy version of ||
.
const or = function(arrayOfConditions){
// Again, we have an array of condition rules
return function(value){
// But this time, when we compare, we only care if any pass.
return arrayOfConditions.some( function(condition){
return condition(value)
}
}
}
// And again, in ES6:
const or = (arrayOfConditions) => (value) => arrayOfConditions.some( (condition) => condition(value) )
That's a lot of code to simply write our own versions of &&
and ||
, but again, it gives us a more "narrative" code.
So far, we've created:
- isEqual
- isNotEqual
- isGreaterThan
- isLessThan
- isNot
- and
- or
Now, using those, we can start to make some compositions! For example, to write our own version of >=
, we simply want to make something that is greater than or equal to. Said differently, isGreaterThanOrEqualTo = or( [isGreaterThan(...), isEqual(...) ] )
. Here goes:
const isGreaterThanOrEqualTo = (comparator) =>(value) => or( [isGreaterThan(comparator), isEqual(comparator) ] )(value)
const isLessThanOrEqualTo = (comparator) => (value) => or( [isLessThan(comparator), isEqual(comparator) ] )(value)
But again, we could simply write if( value >= comparator){...}
and get the same result, right? Sure could. But how about this one?
const isBetween = (min, max) => (value) => and([ isGreaterThan(min), isLessThan(max) ])(value);
That one there? It checks if a value falls within a range.
// In other words, rather than writing:
if( value > min && value < max){ ... }
// we can do this:
const isTwentySomething = isBetween(19, 30)
if( isTwentySomething(value) ){ ... }
Length-of-code-wise, it comes out a wash. If you were paying by the word, I'd say stick with the traditional method. But the tradeoff is, our code begins to read differently.
Whole lot of code... for what?
Again, the point wasn't to minimize our codebase. Instead, the point of this was to look at how we can name variables and functions so they become self-documenting, and how we can begin to compose or combine functions, building from small pieces and building something more elaborate.
This has been a small taste. Here's a fun one, just to show you the power of composability. We now have another function in our toolbelt, isBetween
- let's write a function that checks if a given number is between 10 and 30, or if it's between 50 and 70, or if it's equal to or greater than 100.
To write that out in a traditional statement, we'd get something like:
if( ( value > 10 && value < 30 ) || (value > 50 && value < 70) || value >= 100 ){...}
That works, that's great. Here's how we could compose that:
// first, we write each of our three conditions...
const isBetween10And30 = isBetween(10, 30)
const isBetween50And70 = isBetween(50, 70)
const is100OrMore = isGreaterThanOrEqualTo(100)
// then, we can combine them. We could do this one of two ways:
if( isBetween10And30(value) || isBetween50And70(value) || is100OrMore(value) ) {...}
// or we could combine them with our or() function
const conditions = or([
isBetween10And30,
isBetween50And70,
is100OrMore
])
if( conditions(value) ){...}
And the point is...
There is no right way. There are many roads up the javascript mountain, you find the one that works for you. By doing this, we are building functions that meet our criteria above: they do one thing, they do the same thing each time, they don't have side effects, and they self-describe.