Functional Legos, Part Two
In a previous post, we started building some very atomic functions. To recap those, they looked like:
isEqual(comparator)(value): Boolean
The isEqual function takes a parameter, which it will use as a comparator, and returns a function. That returned function expects a parameter, which it will compare to that comparator. Based on that comparison, it will return a Boolean true/false.
const isEqual = (comparator)=>(value)=>value===comparator
isGreaterThan(comparator)(value): Boolean
The isGreaterThan function takes a single parameter, a comparator, and returns a function. That second function expects a value parameter, which will be compared to the comparator. Based on that comparison, it will return a Boolean true/false.
const isGreaterThan = (comparator) => (value) => value > comparator
isLessThan(comparator)(value): Boolean
The isLessThan curries two parameters, the comparator and value, and returns a Boolean true/false.
const isLessThan = (comparator) => (value) => value < comparator
isNot(Boolean): Boolean
The isNot function takes a single value and returns the Boolean reverse. So a Boolean true will be returned as false, and vice versa.
const isNot = (value) => !value
and(arrayOfConditions)(value): Boolean
The and function takes an array of functions, each of which return a Boolean true/false for a given input value, and returns a function which takes a single value parameter that will be used in each of the conditions. If all the conditions in the array are true, the and function returns true. Otherwise, it returns false.
const and = (conditions)=>(value)=>conditions.every(condition => condition(value) )
or(arrayOfConditions)(value): Boolean
The or condition functions exactly the same as the and function, except that if any condition returns true, the or function returns true.
const or = (conditions)=>(value)=>conditions.some(condition=>condition(value) )
These core functions can be combined in various ways. For example, we can define a isNotEqual
function by simply composing the functions isNot()
and isEqual
:
isNotEqual(comparator)(value): Boolean
isNotEqual is composed of isNot() and isEqual(). Basically, it takes the return value from isEqual and passes it through isNot.
const isNotEqual = (comparator) => (value) => isNot(isEqual(comparator)(value) )
isLessThanOrEqualTo(comparator)(value): Boolean
The isLessThanOrEqualTo combines the isNot and the isGreaterThan. The logic here is, if it is not greater than, then it must be less than or equal to.
const isLessThanOrEqualTo = (comparator) => (value) => isNot(isGreaterThan(comparator)(value))
isGreaterThanOrEqualTo(comparator)(value): Boolean
The isGreaterThanOrEqualTo takes two functions, the isEqual and isGreaterThan, and uses them as conditions in an or function.
const isGreaterThanOrEqualTo = (comparator) => (value) => or(isGreaterThan(comparator), isEqualTo(comparator))(value)
// The same could be done simply by combining the isNot function and the isLessThan:
const isGTE = (comparator) => (value) => isNot(isLessThan(comparator)(value))
isLessThanOrEqualTo(comparator)(value): Boolean
The isLessThanOrEqualTo takes two functions, the isEqual and isLessThan, and uses them as conditions in an or function. Alternatively, we could simply combine the isNot function and the isGreaterThan.
const isLessThanOrEqualTo = (comparator) => (value) => or(isLessThan(comparator), isEqualTo(comparator))(value)
// We could also do this:
const isLTE = (comparator) => (value) => isNot(isGreaterThan(comparator)(value))
isBetween(strict)(min, max)(value): Boolean
The isBetween function does some odd stuff. There are actually two versions of this one, a strict version (which doesn't include min/max), and a weak one (which allows them).
The first call to isBetween expects a true/false value, for strict or not, and returns a function. That function expects two values, for min and max, and returns a function. That final function expects a value, and returns a Boolean for a value in range, based on the strictness required.
// The isBetween simply masks the two actual functions, conditionally returning the appropriate one.
// This one was formatted differently, simply to fit in the browser window.
const isBetween = (inclusive) =>
(min, max) =>
(value) =>
inclusive ? isBetweenWeak(min, max)(value) :
isBetweenStrict(min, max)(value)
// The first excludes the min and max values...
const isBetweenStrict = (min, max) =>
(value) =>
and([
isGreaterThan(min),
isLessThan(max)
])(value)
// ... while the second accepts them as in-range.
const isBetweenWeak = (min, max) =>
(value) =>
and([
isGreaterThanOrEqualTo(min),
isLessThanOrEqualTo(max)
])(value)
Moving on...
Those functions all return a true
or false
value. I have given them descriptive names, that tell you two things: first, that they're a Boolean function (they check if a given value is
something), and second, what the function itself does. isNotEqual()
is very descriptive. Each of those functions was designed for a specific use: a modular approach to writing filter functions.
These are by no means the limit of what we might want in a functional filter library, simply a starting point. But the point of this exercise is to write code that becomes more self-documenting, so let's try an example of how that might look.
// given this array...
const array = [1, 2, 4, 7, 3, 2, 9, 11, 5, 27, -3, 8];
// we can write this, and it works fine.
const filteredArray = array.filter(function(value){
return value<=7;
})
// or with our library of handy functions, we could partially apply
// one, and use that:
const isLessThanOrEqualTo7 = isLessThanOrEqualTo(7);
// we now have a function that is ready to be used in a filter,
// but that also describes its intent.
const filteredArray2 = array.filter(isLessThanOrEqualTo7);
So the second, while it seems a little more verbose, does the same thing. But it also tells us exactly what it is doing. It is self-documenting. As much as possible, it's good practice to use named functions for filter()
, map()
, sort()
and reduce()
, or indeed, nearly any callback function. Let's see another example.
We haven't created one yet, but perhaps we want to filter our elements based on even or odd values. We could simply add this function to our functional filter collection:
// A number is even if, when divided by two, it has a remainder
// of zero.
const isEven = (value) => isEqual(0)(value%2)
// We could also have done:
const isEven2 = (value) => value%2 === 0
So the above function could be used with our same array, exactly as is:
const array = [1, 2, 4, 7, 3, 2, 9, 11, 5, 27, -3, 8];
const filteredArray = array.filter(isEven)
And, just like that, the filter tells us exactly what it's filtering for. But hey, what if we wanted to change that, to filter for even-numbered array indexes? Can we DO that? As a matter of fact, yes:
const array = [1, 2, 4, 7, 3, 2, 9, 11, 5, 27, -3, 8];
// we write a named function, in order to keep filter self-documenting.
const indexIsEven = (value, index) => isEven(index);
const filteredArray = array.filter(indexIsEven)
Remember, a callback passed into a filter takes up to three parameters: a value, an index, and a reference to the original array. In our case, we wrap our isEven()
in a function that takes the first two parameters, and apply it to the second. The value
parameter is never used.
Again, our function, indexIsEven()
, is intended to be very self-documenting. It tells us specifically what it's doing, it tells us that it's doing a comparison and, if we use a consistent naming style, the Is
in there tells us it will return a Boolean.
But what if...?
What if your array was something other than numbers? Would our functions still work? We can test them:
const array = ['Lara','Bill','Bob','Jenny','Bill','Lara','Denise'];
// Simply for the convenience of self-documenting names, let's
// create a partially applied version of the isEqual function here.
const isNamed = (name) => isEqual(name)
// And we can actually apply that function.
const isNamedLara = isNamed('Lara')
console.log( array.filter( isNamedLara ) )
Note that we didn't need the isNamed
function, we could have simply said const isNamedLara = isEqual('Lara')
, but we are writing functions that have meaningful, self-documenting names here!
If we wanted to use our handy-dandy functional filter bits with more complex data types (for example, objects), we need to create a few more useful utility functions. Here are a couple we might find handy:
- a function to retrieve a given property from an object;
- a function to tell us if an object has a given property.
Let's start there:
const prop = (property) => (obj) => obj[property];
const has = (property) => (obj) => obj.hasOwnProperty(property);
Boom. That's all. The first one, prop()()
, takes a property name, then returns a function. That returned function expects an object, and returns the given property name from that object.
How might we use that? Let's play:
const users = [{
first_name: 'Bill',
last_name: 'Tyler',
online: true,
friends: ['Janice','Reuven','Harssha']
},{
first_name: 'Janice',
last_name: 'Ogilvy',
online: false,
friends: ['Bill','Tamika','Nathaniel']
},{
first_name: 'Bill',
last_name: 'Maxwell',
online: false
// Note that he has no friends...
}]
// This line will partially apply our prop function, and return
// us a function that wants an object.
const firstname = prop('first_name')
// Here, we use that function, and it becomes the return value
// for our map method. We're taking the array of users object,
// an converting it to an array of users first names.
console.log( users.map( firstname ) )
Let's use the has function now. We'll partially apply that. We do this by calling the first function, and assigning the returned function to a variable. This is called partial application, as we haven't provided all the parameters to completely apply the function.
// The function that gets returned is assigned to the variable.
const hasFriends = has('friends')
// And now we use that function as our callback.
const folksWithFriends = users.filter(hasFriends)
That last one would return only those users who have a friends
property, thus removing the last Bill.
const propIsEqual = (property) =>
(comparator) =>
(object) =>
isEqual( comparator )( prop( property )( object ) ) )
// The above is three functions, each partially applied. We can
// apply them step-by-step, like this:
const firstNameIsEqual = propIsEqual('first_name')
const firstNameIsBob = firstNameIsEqual('Bob')
// Or we can jump straight to the final step, like this:
const isOnline = propIsEqual('online')(true)
console.log( users.filter( isOnline ) )
Again, our functions names are descriptive and self-documenting. We can tell, at a glance, we're filtering the users array for those users online. We could also continue to compose those functions, by wrapping them in other functions:
// If we want, we can reverse our isOnline.
const isOffline = (object) => isNot( isOnline( object ) )
Note that my isOffline()
function requires a single parameter, and that one is the one required to complete our partially applied isOnline()
function.
Is there a POINT here?
Actually, I think I have flogged that particular horse. Some of these examples have gotten a little silly, and may seem overly complex for simple problems. And you're not wrong. I wrote a complete function to replace the =
, for goodness sake!
And yet... for those who want to be able to glance at code, and have it tell a story, then having descriptive functions that we can reuse, compose, and extend are very useful.
I have tried to use descriptive names for my functions, often sacrificing terseness of code for clarity of description. But, as you become more comfortable with composing functions (combining them one with another, in different combinations and permutations), you may find some functional libraries to be useful.
Much of what I've written here, and in the first Functional Legos post, is very primitive. There are great libraries that do much the same thing, in a far more concise and consistent way. Over the next few weeks, I'll explore one of them: the Ramda FP library.