The Daily TIL

February 25, 2019 by DaveDavejavascriptfunctional

Functional Programming Baby Steps

In a recent project, I was performing some basic array transformations in js. In my PR, someone pointed out that my approach was excessively complex and hard to follow. It was correct, but there was a cleaner way to represent the data transformation. The suggested approach was more functional, compared to my procedural approach. Let’s look at a simple concrete example.

Example

Imagine that we have a line representing values from 0 to 100:

|-------------------------------|
0                              100

Dividers can be placed onto the line to define segments or ranges. The dividers represent points on the line, while the segments represent the spaces between the dividers.

       40             60
|-----------*-------------------|
0           40                 100

In this configuration, there is a divider at the point 40. This results in two segments with values of 40 and 60, respectively. I’ll look at another example before I attempt to derive a pattern.

    20    20        60
|------*-----*------------------|
0      20    40                100       

With a couple of basic examples, the relationship between the dividers and the segments starts to become pretty clear. Given either dividers or segments, we can convert from one to the other.

Dividers to Segments

Given a list of dividers

const dividers = [ 20, 40 ]

We can calculate the resulting segments in a few steps. If we treat the min (0) and max (100) as implicit dividers, the full set of dividers would be

const dividers = [ 0, 20, 40, 100 ]

So the segments are simply the set of values resulting in subtracting a given divider from the previous divider. In this example:

   0 - NaN   20 - 0   40 - 20   100 - 40
   \    /     \   /    \   /      \   /
     NaN        20       20         60

Omitting the first invalid value, we have segment values of

const segments = [ 20, 20, 60 ]

To program this procedurally, we could do something like this:

const procedure = dividers => {
  // define output
  var segments = [];
  
  // add the implicit dividers to input
  var withImplicitDividers = [0, ...dividers, 100];
  
  // iterate through array
  for (var index = 1; index < withImplicitDividers.length; index++) {
    segments.push(withImplicitDividers[index] - withImplicitDividers[index - 1]);
  }
  
  return segments;
}

This is clear enough, but it could be simplified quite significantly using some functional techniques:

const moreFunctional = dividers => 
  [0, ...dividers, 100].map((value, index, values) => value - values[index - 1]).slice(1);

This is really just a shorthand version of the previous approach. We still create a modified starting array containing the implicit dividers. But instead of explicitly iterating, calculating a value, and pushing that value to an output object; we use the map method to perform the same operation for each element in the array.

Segments to Dividers

Converting in the other direction is slightly more complex. Given a list of segments

const segments = [ 20, 20, 60 ]

We can transform the data into dividers by adding each segment to the total of the preceding segments. Again, we can use implicit segments for clarity:

const segments = [ 0, 20, 20, 60 ]

So the calculation would be

   0 + NaN   20 + 0   20 + 20   60 + 40
   \    /     \   /    \   /     \   /
     NaN        20       40        100

The result includes an invalid value and 100, which is one of the implicit dividers. So both of these can be omitted. Procedurally, this could look something like:

const segmentsToDividers = segments => {
  // initialize output
  var dividers = [0];
  
  // add the implicit segment to input
  var withImplicitSegments = [0, ...segments];
  
  // iterate through array
  for (var index = 1; index < withImplicitSegments.length; index++) {
    dividers.push(withImplicitSegments[index] + dividers.reduce((total, current) => total + current));
  }
  
  // trim first and last elements
  return dividers.slice(1, -1);
}

This is more convoluted, but it is still clear enough to follow. We use both the input data and the derived output data to calculate each value. Each time we iterate through the withImplicitSegments array, we sum the element with the total of the elements in the output array. This can be collapsed to be less verbose, though possibly harder to interpret:

const segmentsToDividers2 = segments => {
  // collapsed, but harder to understand
  return [0, ...segments]
    .map((value, index, values) => 
      values
        .slice(0, index + 1)
        .reduce((accum, current) => accum + current)
    )
    .slice(1, -1);
}

This approach works the same as the previous method, but the summation of the preceding elements is performed by taking ever larger slices of the input array. This works, and accomplishes the same thing. But we can still achieve the same result without the added complexity. Let’s use the reduce method a bit more pragmatically for this.

From MDN:

The reduce() method executes a reducer function (that you provide) on each member of the array resulting in a single output value.

So let’s provide a reducer function that returns an array containing the summation of each element and the total of the elements preceding it. That could look something like:

const segmentsToDividers3 = segments => {
  return [0, ...segments]
    .reduce(
      (accum, current, index, src) => 
        [...accum, current + accum[index - 1] || 0], 
      []
    )
    .slice(1, -1);
}

Each iteration of the reducer returns an array containing the summation up to that point. This is basically the same as in the previous example where we slice the output value in each iteration. Again, this results in the same value as the previous examples, but we get to the end result without intermediary variables or additional operations. Everything is contained in the reducer provided to reduce.

The important extra bits are the handling of the invalid cases. The addition of || 0 to the reducer function means that if the result is invalid, use 0 instead. This prevents us from seeding the output with invalid data (as we would in the first iteration). The second parameter to reduce is the initial value for the accumulation array. Again, if this didn’t exist, we would be seeding the result with invalid data.

So here we have a few different ways to approach some simple array manipulation. I haven’t gone into the performance aspects of each variation; I just wanted to highlight how a procedural problem could be solved using a more functional technique. For me, visualizing the data and the transformation in a new way helps to better appreciate the features of a language or a programming style. So yeah, do that.

References