Heavy lifting

Adding new features to an existing function via lifting.

intro

Imagine a simple task: given a javascript plain object, print a property. For example, if the object represents the user information, print the name

1
2
3
4
5
var user = {
name: 'joe'
};
console.log('user name', user.name);
// user name joe

We will refactor this code to be testable and reusable. Let us move the printing into a separate function

1
2
3
4
5
6
7
8
function print(person) {
console.log('user name', person.name);
}
var user = {
name: 'joe'
};
print(user);
// user name joe

guard against nulls

As soon as we have this simple existing single user printing, we will get a few feature requests. First, the program above assumes the user exists. It should check if the variable user is an object with a name property before to be accessing the value. One can code this guard logic inside the print function.

1
2
3
4
5
function print(person) {
if (person && person.name) {
console.log('user name', person.name);
}
}

The guard code is tightly coupled with the print feature. We can split the code apart.

1
2
3
4
5
6
7
8
9
10
11
12
function unsafePrint(person) {
console.log('user name', person.name);
}
function safeProperty(name) {
return function (fn) {
return function (object) {
if (object && typeof object[name] !== 'undefined') {
fn(object);
}
}
}
}

The safeProperty looks a little weird, but it has a useful property: the arguments are in the following order: property name, callback function, object. This order matches the order we expect to discover the information. The property name and the callback are usually known at compile time, while the object is only known at runtime. Here is how we can use it to wrap the print function.

1
2
3
4
5
6
7
8
var print = safeProperty('name')(unsafePrint);
var user = {
name: 'joe'
};
print(user);
// user name joe
print();
// does nothing

I have more examples of similar argument order in the Put callback first for elegance blog post.

support multiple items

The second request you might get after handling null values is to print multiple users. We may add the support for printing multiple items on top of the single print.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function printOne(p) {
console.log('user name', p.name);
}
function print(persons) {
persons.forEach(printOne);
}
var user1 = {
name: 'joe'
};
var user2 = {
name: 'ann'
};
print([user1, user2]);
// user name joe
// user name ann

Imagine we have to support multiple items in each function you write. That would lead to lots of extra code with the same essential functionality. What we really need is an utility function that can convert any function that works with a single item and produces another function that can work with an array of items. Usually such function is called a lift in the functional vocabulary.

1
2
3
4
5
6
7
8
9
function lift(fn) {
return function (arr) {
return arr.map(fn);
};
}
var print = lift(printOne);
print([user1, user2]);
// user name joe
// user name ann

In this case the lift transforms a function that expects and returns a single value. The lift returns another function that expects and returns an array. In a mock type notation we can write what lift does as follows

fn = (* -> *) 
// fn is a function that takes any value and returns a value
lift = (* -> *) -> ([*] -> [*])
// lift is a function that takes (* -> *) function as input
// and returns another function with signature ([*] -> [*]) 

In the print example we do not use the return value from printOne. If the printOne function returned a value, its lifted version would also have a valid return value. As it stands now the print function returns an array of undefined values.

user-friendly API

We have lifted the printOne function, now it works with an array input. It would be nice if I did not need to always pass an array and instead it could work with separate arguments

1
2
var print = lift(printOne);
print(user1, user2);

Let us add a little bit of logic to the lift function to handle both arrays and array-like arguments

1
2
3
4
5
6
7
8
9
function lift(fn) {
return function (arr) {
if (!Array.isArray(arr)) {
arr = Array.prototype.slice.call(arguments, 0);
}
return arr.map(fn);
};
}
var print = lift(printOne);

Supporting both arguments and an array in the lifted function is a nice user-friendly feature and it has a side benefit - the lifted function now works fine even if you don't pass any arguments.

1
2
3
4
5
6
7
8
9
10
print([user1, user2]);
// user name joe
// user name ann
print(user1, user2);
// user name joe
// user name ann
print(user1);
// user name joe
print();
// does nothing

other lifts

Array iteration is a common, but not the only possible feature a function can be lifted to. You can lift a function to add other features, for example guarding against the null values in the first part of this blog post can be a "lift".

We will use the original print function without any checks

1
2
3
function unsafePrint(person) {
console.log('user name', person.name);
}

First, let us wrap a value that can be undefined or null in an object.

1
2
3
4
5
6
7
8
9
10
function MaybeNull(val) {
return {
value: val,
map: function (cb) {
if (this.value) {
return new MaybeNull( cb(this.value) ); // A
}
}
};
}

The MaybeNull type has a property map that runs a given callback function on the value stored inside the MaybeNull object. The returned value is wrapped in the MaybeNull again (// A). It is not necessary for this example but is a good pattern that allows easy chaining.

Now we can use the unsafePrint without worrying about the null or undefined values.

1
2
3
4
5
6
7
var user = {
name: 'joe'
};
(new MaybeNull(user)).map(unsafePrint);
// user name joe
(new MaybeNull()).map(unsafePrint);
// does nothing

Instead of wrapping the value in MaybeNull object and calling map method, let us 'lift' the unsafePrint to make a new function that expects and returns a MaybeNull instance.

1
2
3
4
5
6
7
8
9
10
MaybeNull.lift = function (fn) {
return function (MaybeNullValue) {
return MaybeNullValue.map(fn);
};
};
var print = MaybeNull.lift(unsafePrint);
print(new MaybeNull(user));
// user name joe
print(new MaybeNull());
// does nothing

We can describe the MaybeNull.lift in type notation

fn = (* -> *) 
MaybeNull.lift = (* -> *) -> (MaybeNull -> MaybeNull)

composing lifts

We have two lifts that we can compose and apply to an original function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function unsafePrint(person) {
console.log('user name', person.name);
}
var safePrint = MaybeNull.lift(unsafePrint);
// lift to array function from above
function lift(fn) {
return function (arr) {
if (!Array.isArray(arr)) {
arr = Array.prototype.slice.call(arguments, 0);
}
return arr.map(fn);
};
}
var print = lift(safePrint);
print(new MaybeNull(user1), new MaybeNull(), new MaybeNull(user2));

Function print is a lifted lifted unsafePrint with the following type signature

print = ([MaybeNull] -> [MaybeNull])

Conclusions

Lifting a function using array lift makes the iteration over multiple items a breeze. Similarly, we can lift a function to work with other value wrappers, like null value guard. We can combine several lifts using function composition to get more features if needed.