Fluent API debunked

Intro

I plan to write a serie of articles why Functional programming wins and why it is good for everyone. However I'm not sure about the best order and content distribution. That's why I'm going to create fragments of that serie separately, mess with them, and only afterwards combine them into narrative course.

Code comparison

To begin, let's write some code in a Procedural paradigm.

Procedural

let sortedUsers = users.sort((u1, u2) => u1.name > u2.name ? 1 : -1)
let activeUsers = sortedUsers.filter(u => u.activated)
let activeNames = activeUsers.map(u => u.name)

The code itself is very simple and probably does not deserve explanation. You may ask "Why it's Procedural, if there're methods? Why not Object-Oriented?" You also may ask "Why not Functional, if there're lambdas?"

First: paradigms should be understood in comparison. That code is the most Procedural from examples I'm going to demonstrate. Second: paragims evolve over time. Modern Functional languages are more Functional than CommonLISP which was the most Functional at the moment of creation. Modern Object-Oriented languages are also more Functional than before thankfully to lambda adaptation. I suggest you to read Paradigms in Programming for more information.

Ok. Let's imagine we come back to that example later and notice some problem.

let sortedUsers = users.sort((u1, u2) => u1.name > u2.name ? 1 : -1)
let activeUsers = sortedUsers.filter(u => u.activated)
let activeNames = activeUsers.map(u => u.name)

The row order it not the best. We sort users, than filter them. It will be better in opposite order.
Filtering is going to make array shorter, thereby reducing time to sort.

See how our changes rippled through the lines? We weren't able to just swap two lines. We had to replace three variable names additionally!

Fluent API / Object-oriented

Now to Object-oriented one.

let activeNames = users
 .sort((u1, u2) => u1.name > u2.name ? 1 : -1)
 .filter(u => u.activated)
 .map(u => u.name)

This is a classic fluent interface usage. Quoting Wikipedia

an implementation of an Object-oriented API that
aims to provide more readable code.

Yes, it's more readable as temporary variables don't clog your code anymore. It's also more bug-proof when you reorder lines. No need to rename variables means an entire class of possible errors is eliminated.

Now let's complicate the task a bit more. We discovered that users variable can contain repeating objects and we need to get rid of them. Let's assume this time we have a uniq function which does that for us. Let's apply it after filter again to achieve the best performance.

Line added, downstream variable renamed – just as previous time.

Now with Object-Oriented one. Hmm... looks like we have a trouble :(

OMG that was disruptive! What happened to our pretty code?! You've just saw Expression problem in action. You can't add new behavior to existing class or instance. Well, in "dynamically typed" language you technically can. But it's considered a bad practice to do so in apps and a disaster to do so in libraries.


If you're young and brave and you're thinking that's an overestimation, go and read how the whole ES language specification suffered from MooTools incompatible version of String.prototype.contains. Also read about Prototype vs jQuery and learn from other's mistakes at last.


Even if library provides a specific API to inject new methods you should understand that it's something entirely different from import + function call pair. In first case you mutate global object, while in second namespacing protects you from name collisions.

So Object-Oriented fluent interface turned out to be reliable for reordering but failed miserably when we tried to add behavior that wasn't provided.

You might have thought of fluent interface as the best part of OOP and now I've just broken everything. Don't be frustrated though. If you agree that criticism is valid and tools are screwed you're ready to move further. Better ways to express ideas will make you better programmer indeed.

Functional

Initial version of this may look like

let sortedUsers = sort((u1, u2) => u1.name > u2.name ? 1 : -1, users)
let activeUsers = filter(u => u.activated, sortedUsers)
let activeNames = map(u => u.name, activeUsers)

Which is suspiciously similar to our first Procedural / Object-Oriented example

let sortedUsers = users.sort((u1, u2) => u1.name > u2.name ? 1 : -1)
let activeUsers = sortedUsers.filter(u => u.activated)
let activeNames = activeUsers.map(u => u.name)

only with the reversed argument order.

That's the point where people like to say "Oh, see, there is no difference between Functional and Object-Oriented approaches". I'll prove how wrong they are in a moment.

By moving our data argument into the last position we are able to curry our functions. Assume we did so and sort, filter, map functions are already curried. Then we can pipe data through them. Take pipe as a shell | equivalent. It takes first function and pass users to it. Than it takes it's result and pass it to filter. Then it takes that's result and pass it to map.

What all that give us? Just see

Wait have we just swapped exactly two lines and add exactly one?! Right. No variable renaming, no restructuring. Functional composition works like a charm. You can reorder lines. You can add new behavior. You may avoid temporary variables unless you need them. Perfectly fine.

So what's pipe. It just a simple library function.

// JavaScript
let {compose, pipe} = require("ramda")

compose(doThird, doSecond, doFirst)(data) // compose functions in RTL order and apply to data
pipe(doFirst, doSecond, doThird)(data)    // compose functions in LTR order and apply to data

Currying and functional composition are a huge subject with mathematical roots so it's not expected from you to understand them immediately. Actually, it may take a months to master them.

I'm going to explain that subjects in details in separate articles. The point here is to sneak-peek an idea and arouse an interest, not to teach you any syntax or library.

There are attempts to fix composability problem of Fluent API in JS with functional bind syntax proposal. But they are both a) non-standard yet b) JS-specific, while the article aims to be language-agnostic.

As you probably noticed I did not reason about readability at all. It's subjective while numbers of changed lines and renamed variables are not. I would say that Functional API leads to more or equally readable code than Fluent API or any other you met.

You should also take into attention that provided OOP did benefit heavily from lambdas. In Java <8 you would have to implement Comparator to sort, Filterer to filter or something. So if you're agree that lambdas are great, isn't it curious to see what else Functional paradigm proposes. I assure that we've just scratched the surface!

Summary

There are multiple ways to compose functions. Fluent API popularized by OOP paradigm has significant problems with composability There are ways to compose Object-Oriented and Functional API like macros / placeholder values / wrapper functions but they all come with cost of added abstractions and tend to be "magical". The perfect composability for Functional API comes for free if functions are curried.

Every API you deal with can be classified as "data-first", "data-last" or "mixed". OOP API is "data-first" by definition. Python makes it explicit.

def do_stuff(self, foo, bar): # self <=> this
   print(foo + bar)

Functional API can be any of them but to enable currying it should be "data-last". "mixed" API style is the most "obscure" and should be avoided. Currying is incompatible with default arguments and usually lead to a radically different API than it would be without it.

As a following step I recommend you to see the Underscore, you're doing it wrong presentation which is heavily related to what we've just talked about.

Examples recap

let users = [{id: 1, activated: true}, {id: 2, activated: false}, {id: 1, activated: true}]
// Procedural
let {uniq} = require("ramda")

let activeUsers = users.filter(u => u.activated)
let uniqUsers = uniq(activeUsers)
let sortedUsers = uniqUsers.sort((u1, u2) => u1.name > u2.name ? 1 : -1)
let activeNames = sortedUsers.map(u => u.name)
// Fluent API / Object-oriented
let {uniq} = require("ramda")

let activeUsers = users.filter(u => u.activated)
let uniqUsers = uniq(activeUsers)
let activeNames = uniqUsers
  .sort((u1, u2) => u1.name > u2.name ? 1 : -1)
  .map(u => u.name)
// Functional
let {filter, map, pipe, sort, uniq} = require("ramda")

let activeNames = pipe(
  filter(u => u.activated),
  uniq,
  sort((u1, u2) => u1.name > u2.name ? 1 : -1)
  map(u => u.name)
)(users)