DEV Community

Cover image for Lenses and Partial-application
Tracy Gilmore
Tracy Gilmore

Posted on • Edited on

Lenses and Partial-application

There is a concept from the Functional Programming camp known as lenses that can simplify accessing values (properties) in data structures (objects and arrays). Whilst not directly supported in JavaScript it can be implemented easily in a variety of ways and is an effect technique well worth learning.

To demonstrate the concept we will use another FP technique called partial-application to simplify the task of sorting arrays of objects. We will look at three methods of implementing partial-application. The first mechanism makes use of JS's bind method, second approach uses a closure and recursion. The third utilises the (dreaded) array reduce method.

A short explanation of Partial-Application

In brief the technique of partial-application enables the programmer to supply arguments in stages and execute the function only once all of the required arguments have been supplied. This is in contrast to the more conventional approach of supplying all the arguments at the same time and executing the function immediately.

Benefits of this technique

One of the benefits of this technique is that those parameters that do not change between calls can be supplied once whilst those than change on each call can be provided last minute.

Another, and probably more useful, benefit of this technique is we can effectively define two (or more) interfaces for the function. For example. An array's map method expects a transform function with the interface of (item, index?, array?) where item is each entry in the array, index (optional) is the subscript of the item in the array and array (again optional) is the array itself. We cannot supply additional parameters directly which can limit reuse of the function. Using partial-application we can create the transform function with the expected interface using another function that is supplied with additional arguments, which are in scope (and accessible) within the transform function.

Please add a comment below if you would like me to demonstrate this feature in another post but now back to the original topic.

A refresher on sorting an array

The Array object has a method called sort that anticipates a comparison function used to arrange items in the array (see MDN for more details on sort). The function is called several times during the sort operation, requires two parameters and returns a numeric value according to the following rules:

  • zero indicates the values are the same
  • positive values indicate the items are in descending order
  • negative values indicate the items are in ascending order

Let's check out a simple example using a list of names (strings).

const testData = ['Bob', 'Chris', 'Eve', 'Alice', 'Dave'];

testData.sort((person1, person2) => {
  if (person1 === person2) return 0;
  if (person1 > person2) return 1;
  return -1;
});

console.table(testData);

/* OUTPUT
┌─────────┬─────────┐
│ (index) │ Values  │
├─────────┼─────────┤
│    0    │ 'Alice' │
│    1    │  'Bob'  │
│    2    │ 'Chris' │
│    3    │ 'Dave'  │
│    4    │  'Eve'  │
└─────────┴─────────┘
*/
Enter fullscreen mode Exit fullscreen mode

Now we will 'up the ante' by sorting an array of objects by a slightly nested property.

const testData = [
  { name: 'Chris', dob: { year: 1980, month: 2, day: 1 } },
  { name: 'Bob', dob: { year: 1980, month: 8, day: 5 } },
  { name: 'Eve', dob: { year: 1980, month: 4, day: 2 } },
  { name: 'Dave', dob: { year: 1980, month: 6, day: 4 } },
  { name: 'Alice', dob: { year: 1980, month: 4, day: 3 } },
];

testData.sort((person1, person2) =>
  if (person1.dob.month === person2.dob.month) return 0;
  if (person1.dob.month > person2.dob.month) return 1;
  return -1;
);

console.table(
  testData.map(person => ({
    name: person.name,
    month: person.dob.month,
    day: person.dob.day,
  }))
);

/* OUTPUT
┌─────────┬─────────┬───────┬─────┐
│ (index) │  name   │ month │ day │
├─────────┼─────────┼───────┼─────┤
│    0    │ 'Chris' │   2   │  1  │
│    1    │  'Eve'  │   4   │  2  │
│    2    │ 'Alice' │   4   │  3  │
│    3    │ 'Dave'  │   6   │  4  │
│    4    │  'Bob'  │   8   │  5  │
└─────────┴─────────┴───────┴─────┘
*/
Enter fullscreen mode Exit fullscreen mode

Even with this relatively simple example the comparison function is starting to get a bit messy and repetitious (person_.dob.month). We can simplify it using a technique inspired by Functional Programming's lenses to access object properties.

I this first attempt we create a function that requires one of the items from the array and returns the value of the property we want to sort by. In this example the syntax for the sort comparison is slightly different but the effect is the same. See my note on this aspect towards the end of this post to find out more.

function lookup(person) {
  return person['dob']['month'];
}

testData.sort(
  (person1, person2) =>
    -(lookup(person1) < lookup(person2)) ||
    +(lookup(person1) > lookup(person2))
);
Enter fullscreen mode Exit fullscreen mode

Using the JS bind method

The above comparison function is cleaner and more dynamic but the lookup function just moves referencing of the property out of the comparison function and remains very specific. We can do better by creating a lens (aka lookupGenerator in the following examples) using partial-application.

In the following example we will use the JS OO facility bind to apply, partially, lookupGenerator to create the lookup function.

function lookupGenerator(prop1, prop2, obj) {
  return obj[prop1][prop2];
}

const lookup = lookupGenerator.bind(null, 'dob', 'month');
Enter fullscreen mode Exit fullscreen mode

When the lookupGenerator function is called it is supplied with arguments to populate the first two properties prop1 and prop2 but not the third. Using the bind method returns a new function that is assigned to lookup. The new function only requires the third parameter to be supplied in order for the lens to operate.

The sort operation does not change, supplying the lens with the specific items out of the array that require comparing. Not how we satisfied the parameters (partially-applied the arguments) of the lens in two stages with the second being within the sort comparison function.

Using JS closure and recursion

The lookupGenerator is still rather specific so here is another way of implementing a lens through partial-application using a closure, recursion along with rest and spread operations. This approach is more complicated but is far more dynamic and reusable.

function lookupGenerator(...props) {

  const _lookupGenerator = (obj, prop, ...props) =>
    prop ? _lookupGenerator(obj[prop], ...props) : obj;

  return obj => _lookupGenerator(obj, ...props);
}

const lookup = lookupGenerator('dob', 'month');
Enter fullscreen mode Exit fullscreen mode

In the above implementation of the lookupGenerator lens we start by providing all of the properties (in sequence) required to locate the property we want to sort by but this time there can be any number of arguments and they are defined by the use case not implementation. The recursive process keeps calling _lookupGenerator until all the supplied parameters are exhausted before returning a function to accept the final argument (the object) and execute the function to retrieve the value of the property within it.

Using the Array reduce method

The third and final approach might be shorter but the fact it uses the Array reduce method can make it appear more complicated. However, all that is happening here is the array on which the reduce is being performed is the list of properties for the object lens.
The starting value of the accumulator is the object in focus. It still employs partial-application because the list of properties is passed on the first call, a function is returned. When the generated function is called it is passed the subject object and (if found) returns the value of the property.

function lookupGenerator(...props) {
  return obj =>
    props.reduce((o, p) => 
      p in o ? o[p] : null, obj);
}
Enter fullscreen mode Exit fullscreen mode

The last two examples (above) have the advantage that the generator can be reused and supplied with a variety of arguments. For instance we can even reference array subscripts as follows.

const dayOfFourth = lookupGenerator('3', 'dob', 'day');

console.log(dayOfFourth(testData));

// Fourth entry is 'Dave' with a day of birth of '4'
Enter fullscreen mode Exit fullscreen mode

Conclusion

While this sort example is rather simplistic I think it demonstrates adequately how lenses can simplify code such as the comparison function but providing a mechanism for locating properties deeply nested in objects and arrays.

Using the bind method to create the lens demonstrates the concept but is rather limiting and specific (not reusable.) The recursive and reduce approaches might be more difficult to understand but are far more reusable.

The code illustrated in this post is not recommended for use in production but the concepts most certainly are. Libraries like lodash and underscope provide many tried and tested, production-ready functions, some from the FP camp, that can simplify your code and make it easier to create reusable code.

Finally, a note on the comparison function (I did promise)

Whilst writing this post I found I could write the comparison function as follows.

(person1, person2) =>
  -(person1 < person2) || +(person1 > person2)
Enter fullscreen mode Exit fullscreen mode

This is a technique I have not seen anywhere else and not used before myself, so I conducted some additional testing and found it worked. However, I am sure there are undiscovered pros and cons. There is a mix here of Boolean logic, (lazy) numeric evaluation and type coercion that TypeScript might object to but is sound JS.

How it works

The Boolean values true and false coerce to numeric values 1 and 0 respectively, so the numeric comparisons (less than and greater than) will first return a Boolean value before being converted to numeric values -1|0 and +1|0 respectively.

The logical or (||) performs lazy evaluation so if the two values being compared are in (less than) order the second (greater than) expression will not be performed and -1 will be return immediately. If the values being compared are equal both sides will result in 0 and zero will be returned (not false as might be suspected).

Supporting code for this post can be found at JSFiddle including some proof tests for my comparison function.

Supplemental

There is a supplementary post to this to describe an enhancement to the code.

Top comments (0)