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 | var user = { |
We will refactor this code to be testable and reusable. Let us move the printing into a separate function
1 | function print(person) { |
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 | function print(person) { |
The guard code is tightly coupled with the print feature. We can split the code apart.
1 | function unsafePrint(person) { |
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 | var print = safeProperty('name')(unsafePrint); |
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 | function printOne(p) { |
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 | function lift(fn) { |
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 | var print = lift(printOne); |
Let us add a little bit of logic to the lift
function to handle both arrays and array-like arguments
1 | function lift(fn) { |
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 | print([user1, user2]); |
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 | function unsafePrint(person) { |
First, let us wrap a value that can be undefined or null in an object.
1 | function MaybeNull(val) { |
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 | var user = { |
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 | MaybeNull.lift = function (fn) { |
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 | function unsafePrint(person) { |
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.