Pass the logic

How to pass logic instead of data in functional programming.

Imagine you have a function, and it grabs the keywords from a typical NodeJS package definition file package.json. If you want, you can make a module like this using npm init -y command.

package.json
1
2
3
4
5
6
{
"name": "grab-keywords",
"description": "example module",
"version": "1.0.0",
"keywords": ["foo", "bar", "baz"]
}

We can implement this function like this - the simplest way possible

index.js
1
2
3
4
function grabKeywords(filename) {
return require(filename).keywords;
}
module.exports = grabKeywords;

We can confirm this logic is working by writing a few unit tests

spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
describe('grab keywords', () => {
const grab = require('./index')
it('is a function', () => {
console.assert(typeof grab === 'function')
})
it('works', () => {
const keywords = grab('./package.json')
// expected ["foo", "bar", "baz"]
console.assert(Array.isArray(keywords), 'returns an array')
console.assert(keywords.length === 3, 'with 3 items')
console.assert(keywords[0] === 'foo', 'first item is foo')
})
it('returns nothing if there are no keywords', () => {
const keywords = grab('./test.json')
console.assert(typeof keywords === 'undefined')
})
})

The file test.json is very simple

1
2
3
{
"name": "a test without keywords"
}

Notice I am using bits of ES6 in my code, which is ok.

What happens if I want to extend this code and return a default value if the file does not exist, or does not have keywords property? Let us leave aside non-existent files

  • it probably is a runtime exception, and handle the case when the returned object does not have the keywords property. We can handle this case every time a call to `grabKeywords1 is made
1
2
3
4
var keywords = grabKeywords('./test.json');
if (typeof keywords === 'undefined') {
keywords = [];
}

Ok, but this means every call probably will have to add this piece of code, including the if branch, making it more complex.

If we push the default value check into the function itself, we can avoid the code duplication, giving the user a new feature at very little cost.

At first, we could have the default value right inside the function

1
2
3
function grabKeywords(filename) {
return require(filename).keywords || [];
}

But this is very hard-coded. What if the users consider [] to be a wrong default value? One is tempted to extend the grabKeywords function with second parameter - the default value the user can optionally receive.

index.js
1
2
3
4
function grabKeywords(filename, defaultValue) {
return require(filename).keywords || defaultValue;
}
module.exports = grabKeywords;

We are passing the default value (data) into the function and we had to add some more logic (behavior === code) to the function itself: we had to add the logical OR operator || defaultValue.

Notice that the implementation || defaultValue is very subtly different from the typeof keywords === 'undefined' logic, or even what we probably meant to do. The || defaultValue will return the default value in many cases, like 0 || defaultValue or "" || defaultValue. Remember, the logical OR is the equivalent to the casting == operator. In JavaScript, checking if an object does not have a property should be hasOwnProperty method.

1
2
3
4
5
6
7
function grabKeywords(filename, defaultValue) {
var pkg = require(filename);
if (!pkg.hasOwnProperty('keywords')) {
return defaultValue;
}
return pkg.keywords;
}

Here is where things become very hairy. Some users would want the above hasOwnProperty check before returning the default value. Others might want the simple || defaultValue check because they might consider null, 0 or "" values to be non-existent.

So what should we implement inside the grabKeywords function? Should it be hasOwnProperty or || defaultValue or some other check?

Here is where the functional programming principle comes in handy. Instead of passing the data defaultValue into the grabKeywords function and implementing "One True Behavior", we can easily pass custom behavior (a function) instead. Here is one implementation

1
2
3
4
function grabKeywords(filename, defaultValue) {
var keywords = require(filename).keywords;
return defaultValue(keywords);
}

Notice that instead of passing call-time value, we passed a function that can intelligently decide what the returned value should be. The decision could be very simple: an identity function works for most cases that needed the initial logic and nothing else.

1
2
3
4
5
6
7
8
9
10
function I(x) { return x; }
function grabKeywords(filename, defaultValue) {
if (typeof defaultValue !== 'function') {
defaultValue = I;
}
var keywords = require(filename).keywords;
return defaultValue(keywords);
}
// grabKeywords('./test.json') returns "undefined"
// grabKeywords('./package.json') returns ["foo", "bar", "baz"]

For other cases, we could pass client side function to return a default value a client decides at the very last moment.

1
2
3
4
5
function default42(value) {
return typeof value === 'undefined' ? 42 : value;
}
const keywords = grab('./test.json', default42)
// keywords will be 42

Boom! The library's author does not have to decide for everyone for all the eternity what the value comparison logic should be. Instead a provided function will be called, and it can make the decision in whatever manner the client needs. For example, one can implement a logic to assume that 3 keywors is NOT enough.

1
2
3
4
5
6
7
function needAtLeast4(value) {
return Array.isArray(value) && value.length > 3 ?
value : ['not', 'enough', 'key', 'words'];
}
const keywords = grab('./package.json', needAtLeast4)
// keywords will be ['not', 'enough', 'key', 'words']
// because ['foo', 'bar', 'baz'] is not enough

FP vs OOP

A quick note why passing a function around is better than some software pattern from the "Gang of Four" book. A single function is a very simple entity. It has only 1 possible action - calling it. No need to initialize an object in some specific way, or create a callback object, etc. In most cases one can even see what a function expects by calling fn.toString() and reading the source code, especially its signature line.

This is it. The functional programming is all about passing behavior (functions) around, rather than hardcoding it. One can argue with the implementation in this example, but it is simple to demo the point, not be an ideal code.