The Beauty of Javascript Composition

I've been heavily into functional programming with Javascript for quite some time now. Every new line of code I write takes advantage of ES6's shorthand syntax and functional programming techniques. When updating existing code, I'll generally use the opportunity to refactor it to a more functional style. But perhaps the greatest benefit is function composition, the process of combining two or more functions to produce a new function.

Function composition lets us combine multiple functions into steps that transform our data as it flows through them. It's like an assembly line where each step alters the data in some way. Technically you don't need to use functional code to create a composable function, but when you do, the result is clean, elegant, easily reasoned, and beautiful code.

An Imperative Approach

Let's say I have an article object that contains three keys: keywords, entities, and concepts. Each key contains an array of objects that contains several keys including relevance and text. I want to extract the text from each item if the relevance is greater than 0.7. Then I need to dedupe them using a case insensitive comparison. Also, entities sometimes contain disambiguated forms of the text, so I need to use that if available. If I were to do this with imperative programming, it might look like this:

var tags = []; for (var keyword in article.keywords) { if (keyword.relevance > 0.7) { tags.push(keyword.text); } } for (var concept in article.concepts) { if (concept.relevance > 0.7) { tags.push(concept.text); } } for (var entity in article.entities) { if (entity.relevance > 0.7) { tags.push(entity.disambiguated ? entity.disambiguated.text : entity.text); } } var uniqTags = []; for (var tag in tags) { if (uniqTags.indexOf(tag.toLowerCase()) === -1) { uniqTags.push(tag.toLowerCase()); } }

There are plenty of other ways to accomplish this, such as creating a deduping function. The code above isn't overly complicated. We easily handled the disambiguated condition with a simple ternary operator and our code is fairly easy to reason about. However, that is a lot of code for such a simple task. We need to tell the system to LOOP over each array and then give them specific instructions as to what to do during each iteration. During each iteration we are modifying a variable, which in a small script like this isn't a big problem, but side effects in large scripts can be very difficult (and frustrating) to debug.

A More Functional Approach

Now look at the example below. I'm using the Ramda.js functional programming library so I don't have to write my own compose function. Ramda.js also has a lot of other handy functions that make life a lot easier.

const R = require('ramda') let tags = R.compose( R.uniqBy(R.toLower), => x.disambiguated ? : x.text), R.concat(article.keywords.filter(x => x.relevance > 0.7)), R.concat(article.entities.filter(x => x.relevance > 0.7)), R.concat(article.concepts.filter(x => x.relevance > 0.7)) )([])

Isn't this much cleaner and more elegant? If another programmer looks as this, they can easily reason about this code and understand exactly what it is doing:

  1. Line 3: This is a composed function, so it processes in reverse order (you could also use R.pipe if you wanted it to process the other way)
  2. Line 9: Pass an empty array into the composed function
  3. Line 8: Filter articles.concepts, including only items where relevance is greater than 0.7 and then concatenate that with the empty array passed in
  4. Line 7: Do the same thing as line 8 with articles.entities
  5. Line 6: Do the same thing as line 8 with articles.keywords
  6. Line 5: Map over the array and return just the text or the disambiguated name if it exists
  7. Line 4: Use Ramda's uniqBy function to create a deduped array by comparing the lowercase versions of each item in the array

This code is so simple that it took me less than 2 minutes to write. It is also pointfree, meaning that there are no intermediate variables that are manipulated. The data just flows through and returns a final result with no side effects. This code tells the interpreter WHAT to do, not HOW to do it like our imperative example.

Become a Better Programmer

There is a bit of a learning curve to understanding functional programming, but once you get it, you'll never go back to your old ways. If you're curious about the status of your data at any step in the composition, you can simply use Ramda's tap method to log the current state. If you need to create more complex transformations, you can write your own composable functions and add them as additional steps.

Being able to quickly write easily-reasoned, bulletproof code is paramount to your efficiency and efficacy as a programmer. Stitching together small, independently-testable, pure functions will not only take you to the next level as a developer, but will help you to create beautifully written code that your team members and future developers will love you for.

Comments are currently disabled, but they'll be back soon.