Codementor Events

Keep Your Composition…Clean

Published Apr 25, 2018Last updated Oct 22, 2018

The takeaway from the last post is that if you can compose your code you should. Consider it an extension of other, less controversial maxims like: use map when transforming a collection into another and reduce to convert a collection into a “single” value.

To be frank, I’d be pleased to see the code on the left in a pull request. But since I’ve got an ax to grind, I’d ask the submitter to think about why we need the anonymous function around the logging statements. Our power of reading tells us it’s because an additional argument is needed to set the logging level ( remoteLog has an arity of two instead of one). My straw-man dev faithfully returns with a higher level function that accepts a logging level and returns an inner function taking a single argument:

function remotelogUnary(level){ 
 return msg => remotelog(level, msg)
}

function functionalWay(color) {
fetch(color)
.then(JSON.parse)
.then(transformFirst)
.then(sendToClient)
.then(remotelogUnary('log')).catch(remotelogUnary('error'))
}

remotelogUnary('error')('gross!')

That “solves” the problem I posed, but we lose more than we gain. Every function that requires more than one parameter now needs sistering: remoteLog and remotelogUnary! Plus in order to do a remoteLog call outside of the pipeline I actually have to call both functions (which is gross).

But again, in our functional paradise, applying only some of the arguments required by a function and then returning another function that accepts the remainder is a super generic operation called partial application. And once we write a function to partially apply arguments to another, we need never do it again! In real life, we don’t write it at all. Functional languages include it as part of the standard library (some like Haskell do something like it to every function). In JavaScript we get this from libraries like ramda and underscore. Nonetheless I include a three-line ES6 version below:

const remotelog = partial((level, msg) => { console[level](msg)})

function functionalWayBest(color) {
  fetch(color)
    .then(JSON.parse)
    .then(transformFirst2)
    .then(sendToClient)
    .then(remotelog('log'))
  .catch(remotelog('error'))
}

functionalWayBest('blue')
remotelog('log', 'testme')
remotelog('log')('another')

function partial (fn, ...cache) { return (...args) => {
  const all = cache.concat(args);
  return all.length >= fn.length ? fn(...all) : partial(fn, ...all);
}}

For maximum flexibility, the above construction gives us a single function that can be called with one argument twice, or both arguments once. It’s super common to see functions assigned to constants like this in code bases that use ramda or underscore. Now you know why. There’s a downside, though: functions assigned to constants don’t get hoisted. So, often, helper functions get declared before the code that calls them (In the above code I’d prefer to see the remotelog function below the main driver function, functionalWayBest ).

Instead of coming back with a partially applied function, the dev might have come up with something like this

function RemoteLog(level){
  this.level = level
  this.log = msg => console[this.level](msg)
}

To me that looks a lot like a class with dependencies loaded in on the constructor. With a little imagination, it should look familiar to someone who writes a lot of java code ( ILookLikeAJavaClass ). Granted, this a somewhat contrived example, but it does show how often when we believe we are thinking in terms of objects, we are really just sharing state among a bunch of methods.

function RemoteLogConstructed(level){
  this.level = level
  this.log = msg => console[this.level](msg)
}

function ILookLikeAJavaClass(level){
  this.level = level
  this.one = msg => console[this.level](msg)
  this.two = msg => console.log('Do something else')
}

function RemoteLogA(level){
  this.log = msg => console[level](msg)
}

function RemoteLog(level){
  return msg => console[level](msg)
}

function withConstructor(color) {
  let goodLogger = new RemoteLogConstructed('log')
  let errorLogger= new RemoteLogConstructed('log')
  fetch(color)
    .then(JSON.parse)
    .then(transformFirst2)
    .then(sendToClient)
    .then(goodLogger.log)
  .catch(errorLogger.log)
}

I don’t like having to name two variables, the distance between declaration and use, having to call the constructor based version with new (see what happens if you don’t!), and having to make two calls whenever I want to use them.

There’s a deeper lesson here. When I think in terms of class hierarchies, I’m explicitly thinking in terms of the general and the particular. Passing in the dependencies on the constructor is a step on the way to composition and classical OO languages, it’s an important way, but I feel it’s not idiomatic in javascript (ES6 classes be damned). Partial application is temporal, and makes me think about things in terms of “before and after.” I’m not saying that “before and after” should always replace “general and particular’. But that the temporal metaphor is an additional way of solving problems and organizing code. Adopting it can be surprisingly liberating.

On another tack, if the anonymous functions in the pull request included a function body (the curly braces) I would have barked for real and insisted they be removed.

function functionalWay(color) {
   fetch(color) .then(JSON.parse) .then(sendToClient)
   .then(sendResults => {remotelog('log',sendResults)}) .catch(err => {remotelog('error', err)})

It’s not just that they are unnecessary. Its that when you give somebody access to a function body, they’ll invariably fill it up with statements. The above all too easily becomes

.then(sendToClient).then(sendResults => { doSomethingElse(sendResults) remotelog('log',sendResults) })

If doSomethingElse is a step in the pipeline, pull it up to to the same level as sendToClient. If it’s not a composition, maybe it doesn’t belong there at all. And if it performs some side-effect, you can make it composable with the tap function we learned about in the last post.

Left as is, doSomethingElse is a loose cannon! If it mutates its argument in an unexpected way, remotelog may break. Immutability is important, but as long as I am strictly composing my code, the lack of it seldom bites me on the ass. So, in the end I greatly prefer something like this:

function functionalWay(color) {
  pipeP(fetch,
      JSON.parse,
      transformFirst,  
      sendToClient,  
      remotelog('log')
      )('blue')
      .catch(remotelog('error'))
}

function pipeP (...fns) {
  const start = fns.shift()
  return (...args) => fns.reduce(chain, new Promise(res => res(start(...args)))) 
}

function chain (q, fn) {return q.then(fn)}

It unambiguously (some might say, aggressively) declares itself a composition. Collaborators are encouraged to add steps in the compositional pipeline, and discouraged from polluting the function body with additional statements. It is open for extension and closed for modification without the heavy stage machinery of protected and private functions and class hierarchies.

As an added benefit one can freely mix promise returning functions with regular ones (without lexical overhead of the .then function). Consider it my gift to the reader.

Finally, the functions in the argument list passed to pipeP can just as easily be written as an list/array. This opens up entirely new vistas of composition! We can pass the list around and add and remove functions to tailor our pipeline for different scenarios. Often I’ll use this technique to swap out code that has side-effects for testing or to provide adapters for concrete consumers and producers.

The code for this piece can be found here: https://repl.it/@rmoskal/asyncharmful2. The first installment can be found here. If you enjoyed one or both of these pieces, encourage me to write a third by applauding. I would address a few other things like organizing this sort of code into larger units, handling variable configurations, alternate ways of sharing state among methods, and anything that might come up in the comments.

Discover and read more posts from Robert Moskal
get started
post commentsBe the first to share your opinion
Show more replies