The versatility of Array methods

Using array methods to replace conditionals, null checks and exception handling.

Javascript arrays have some pretty useful helper methods. Usually we think of them as a handicapped Lodash or Ramda. Sure map and reduce is nice, but what about _.uniq or _.difference or _.groupBy.

But Javascript has just enough building blocks to get by without pulling in additional libraries. And in es6 with arrow functions we can define new and interesting visitor functions without expending too much effort.

But what I’d like to focus on in this post, is seeing these existing Array methods in a new light, as a replacement for if statements.

Dessert First

By the end of this article you are going to be able to convert this code:

var results = []
for( var i = 0; i < list.length; i++ ){

  var a = list[i]
  var b = a && a.someProperty && a.someProperty.someSubProperty
  try {
    var c = JSON.parse(b).value
    if( c % 2 == 0){
      results.push( c )
    }
  } catch ( e ){}
}

Into this code:

const f = 
  pipe(
    flatMap(
      pipe(
        get('someProperty') 
        ,flatMap(get('someSubProperty')) 
        ,flatMap(parseJSON)
      )
    )
    ,filter( isEven )
  )
  
const results = f(list)

We are going to replace a lot of tedious spaghetti code with small isolated functions that build upon functionality already present in Javascript Array methods.

Operations that may not succeed

function divide(a, b){
  return a / b
}

function get(p, o){
  return o[p]
}

function parseJSON(str){
  return JSON.parse(str)
}

function toLowerCase(str){
  return str.toLowerCase()
}

Above we have some utility functions that seem fairly innocuous.

toLowerCase('{"A": 2}') //=> '{"a": 2}'
parseJSON('{"a": 2}') //=> { a: 2 }
get('a', { a: 2}) //=> a
divide(10, 2) //=> 5

But all these functions could potentially fail or yield unexpected output.

toLowerCase(null) //=> TypeError: Cannot read property toLowerCase of null
parseJSON("{'a': 1 }") //=> SyntaxError: Unexpected token ' in JSON at position 1
get('a', { A: 2 }) //=> undefined
divide(10, 0) //=> Infinity

So we could make our functions more defensive:

function toLowerCase(str){
  if( typeof str == 'string' ){
    return str.toLowerCase()
  } else {
    // what goes here?
  }
}
function parseJSON(str){
  try {
    return JSON.parse(str)
  } catch (e){
    // what goes here?
  }
}
function get(p, o){
  if( o != null ){
    return o[p]  
  } else {
    // what goes here?
  }
}
function divide(a,b){
  if( typeof a == 'number' && typeof b == 'number' && b !== 0){
    return a / b
  } else {
    // what goes here?
  }
}

Even though our simple functions now check values diligently for errors and catch potential exceptions we still have problems!

The last point is the most important. What is the correct return type for the failure case?

What is a reasonable response to any of the else or catch cases?

As a thought experiment, lets create a rule that an empty list symbolizes failure, and a list of one entry symbolizes success.

[1] //success
[] //failure

Let’s create a decorator function that absorbs exceptions and wraps the result in a list.

function unsafeToMaybe(f){
  return function(x){
    try {
      return [f(x)]
    } catch (e) {
      return []
    }
  }
}

If we wrap JSON.parse in our unsafeToMaybe we can write code that can traverse the success case without worrying about the failure case.

const parseJSON = unsafeToMaybe(JSON.parse)

parseJSON('{"A":1}') //=> [{A:1}]
parseJSON("<A>1</A>") //=> []

Now that we get back a list, we can take advantage of the native behaviour of map. It will never call a function on an empty list, and in our case an invalid output.

parseJSON('{"A":1}').map( o => o.A / 2 ) //=> [0.5]
parseJSON("<A>1</A>").map( o => o.A / 2 ) //=> []

Let’s extend our unsafeToMaybe to ignore undefined and null too.

function unsafeToMaybe(f){
  return function(x){
    try {
      // no if statements here!
      return [f(x)].filter( v => v != null )
    } catch (e) {
      return []
    }
  }
}

Now let’s make our original functions all return lists on a failure case.

// we'll leave divide be as technically `Infinity` is a correct response
// but we'll curry it so we can easily call it when we're chaining
const divide = a => b => a / b

// we'll leave toLowerCase as is
// because we don't need to do null checks with our new system
function toLowerCase(str){
  return str.toLowerCase()
}

// We only need to wrap the unsafe part
const get = p => unsafeToMaybe( o => o[p] )

const parseJSON = unsafeToMaybe(JSON.parse)

Notice we’ve only wrapped functions that could either return undefined/null or could throw an exception based on non null input. We are no longer checking if the inputs are valid: we can always safely assume they are from here on in.

['{"A":2}'] 
  .map( toLowerCase ) //=> ['{"a":2}']
  .map( parseJSON ) //=> [[{ a: 2 }]]
  .map( v => v.map( w => w.map( get('a') ) )) //=> [[[2]]]
  .map( v => v.map( w => w.map( x => x.map(divide(100)) ))) //=> [[[[50]]]]
  

So we’re gracefully handling error cases, but we’ve got a problem. Everytime we call a function that could potentially fail we end up with an extra wrapper list. There’s an easy fix, we need a function that can transform our value and then unwrap 1 layer of list at the same time. This function goes by many names, some call it bind, others call it chain, we’ll call it flatMap.

Array.prototype.flatMap = function(f){
  // We don't just grab `[0]` because 
  // Array's can hold more than one value.
  return this.map(f)
    .reduce( (p,n) => p.concat(n), [])
}

[3].flatMap( x => [x * 2] ) //=> [6]

With flatMap in play let’s revisit our sample caller code.


// valid json
['{"A":2}'] 
  .map( toLowerCase ) //=> ['{"a":2}']
  .flatMap( parseJSON ) //=> [{ a: 2 }]
  .flatMap( get('a') ) //=> [2]
  .map( divide(100) ) //=> [50]
  

// invalid json
["{'A':2}"] 
  .map( toLowerCase ) //=> ["{'a':2}"]
  .flatMap( parseJSON ) //=> []
  .flatMap( get('a') ) //=> []
  .map( divide(100) ) //=> []
  

// valid JSON with missing property 'a'
['{"B":2}'] 
  .map( toLowerCase ) //=> ['{"b":2}']
  .flatMap( parseJSON ) //=> [{ b: 2 }]
  .flatMap( get('a') ) //=> []
  .map( divide(100) ) //=> []

// valid JSON dividing by 0
['{"A":0}'] 
  .map( toLowerCase ) //=> ['{"a":0}']
  .flatMap( parseJSON ) //=> [{ a: 0 }]
  .flatMap( get('a') ) //=> [0]
  .map( divide(100) ) //=> [Infinity]

Take a long look at the above code. In this new world we no longer see undefined, null, SyntaxError or any other warts. And we aren’t ending up with deeply nested lists of values. We can also tell which functions will potentially fail because we call them with flatMap as opposed to map.

There’s 2 niggling issues

Well we could technically solve the first issue by putting an if statement in divide that returns null, but that would break our rule of avoiding if and avoiding null.

There’s a few strategies we can take for handling input that’s invalid for our business logic but valid within the language (e.g. Infinity or NaN)

We’ll visit them momentarily, but first let’s stop hacking on the prototype.

const flatMap = f => x => 
  x.map( f ).reduce( (p,n) => p.concat(n), [])
  
const map = f => x =>
  x.map(f)

const pipe = (...fs) =>
  x => fs.reduce( (x,f) => f(x), x)
  
const f = pipe(
  map( toLowerCase ) //=> ['{"a":2}']
  ,flatMap( parseJSON ) //=> [{ a: 2 }]
  ,flatMap( get('a') ) //=> [2]
  ,map( divide(100) ) //=> [50]
)

f(['{"A":2}'] ) //=> [50]

Strategies for handling invalid values for your business logic

const divide = a => b => a / b

Mathematicians often have different answers what dividing by 0 yields. It can vary from person to person or from field to field. In a business environment we usually want to consider 0 as an invalid case, and in some languages dividing by 0 throws an Exception. In JS it returns Infinity. Infinity is a Number and doesn’t throw Exceptions when we operate on it, but depending on your situation that may actually be a bad thing, because it can silently fail.

( 2 / 0 ) // => Infinity
( 2 / 0 ) * 100 // => Infinity (No Exception, because Infinity is a valid number)

One solution is to handle the case ourselves:

;[0]
  .filter( x => x != 0 )
  .map( divide(100) ) //=> []
  

;[2]
  .filter( x => x != 0 )
  .map( divide(100) ) //=> [50]

Another solution (popular in F#) is to create a new type that defines what is valid and what isn’t.

const NonZeroNumber = x => 
  x != 0 
  ? [x]
  : []

This may not be the way your used to seeing types defined, you may have expected a class with some inheritance, but this achieves exactly the same thing without exceptions, Infinity or if statements in our business logic.

NonZeroNumber(2)
  .map( divide(100) ) //=> [50]
  
NonZeroNumber(0)
  .map( divide(100) ) //=> []

BONUS ROUND: Multiple parallel conditionals

Have you ever seen logic like this before?

if( a && b && c && d ){
  // do something
}

Or this logic:

if( a || b || c || d ){
  // do something
}

Or maybe something like this:

a = a || defaultValue

Or potentially:

a = b && b.property && b.property.nestedProperty || defaultProperty

We can replace all of these patterns with Array methods like some, every and find

if( a && b && c && d ){
  // do something
}

Becomes:

[[a,b,c,d]]
  .filter( conditions => conditions.every(Boolean) )
  .map( doSomething )

if( a || b || c || d ){
  // do something
}

Becomes:

[[a,b,c,d]]
  .filter( conditions => conditions.some(Boolean) )
  .map( doSomething )

a = a || defaultValue

Becomes

a = [a,defaultValue].find(Boolean)

a = b && b.property && b.property.nestedProperty || defaultProperty

With our flatMap and get function becomes:

[b]
  .flatMap( get('property') )
  .flatMap( get('nestedProperty') )
  .concat( defaultProperty )
  .find( Boolean )

A simpler language

Even if your not 100% sold on using Array methods to avoid for loops, if statements, exceptions, unwanted values and more. I hope I’ve made the case that Array methods are versatile, and perhaps next time you have some complex logic involving possibly null values and functions that may throw exceptions, you might give a thought to the humble Javascript Array, the little object that could.

This article has been a covert introduction into the Maybe and Either Monad. If you want to try out the real thing check out Sanctuary

I also recommend checking out Railway Oriented Programming by Scott Wlaschin. It’s a helpful introduction to the conceptual framework and justifications for avoiding conditions and exceptions.

By relying on types that encapsulate decisions, branching, failure, success, we can write simpler code that is easier to edit and less likely to crash.

Thank you, for your time and happy coding!


Many thanks to Barney Carroll for transforming my mental sprawl into coherent, readable English.


Thank you for reading. If you’d like to reach out you can get in touch on Mastodon, BlueSky or Twitter.

Recent articles

Previous articles

Feeds