Search code examples
javascriptfunctional-programmingramda.jspointfree

When is it appropriate to choose point-free style vs a data-centric style in functional programming?


In case it matters this is about functional programming in JavaScript and in my examples I’ll be using Ramda.

While everybody at work has fully embraced functional programming, there’s also a lot of discussions around how to do it “right”.

These two functions will do exactly the same thing: take a list and return a new list in which all strings have been trimmed.

// data-centric style
const trimList = list => R.map(R.trim, list);
// point-free style
const trimList = R.map(R.trim);

So far so good. However with a more complex example, the difference between the two styles is striking: take a list and return a new list in which all strings are equal to a property found in an object.

var opts = {a: 'foo', b: 'bar', c: 'baz'}; 
var list = ['foo', 'foo', 'bar', 'foo', 'baz', 'bar'];

myFilter(opts, 'a', list); //=> ["foo", "foo", "foo"]
myFilter(opts, 'b', list); //=> ["bar", "bar"]
// data-centric style
const myFilter = (opts, key, list) => {
  var predicate = R.equals(opts[key]);
  return R.filter(predicate, list);
};
// point-free style
const myFilter = R.converge(
  R.filter, [
    R.converge(
      R.compose(R.equals, R.prop), [
        R.nthArg(1),
        R.nthArg(0)]),
    R.nthArg(2)]);

Besides readability and personal taste, are there any reliable evidences to suggest that one style is better suited than the other in some circumstances?


Solution

  • I know of no evidence that demonstrates advantages of one style over the other. But there is a clear trend in the history of programming towards higher abstractions... and an equally clear history of resistance to this trend. A move from Assembly to Fortran or LISP was a move up the abstraction stack. The use of SQL rather than bespoke B-tree retrievals was a another. The move to FP, both within a language like Javascript and in the changing landscape of programming languages, is to my mind a similar move.

    But much of that has to do with elements more fundamental than this syntactic decision: equational reasoning means that we can build our own abstractions on top of more solid footing. So purity and immutability are essential; point-free is merely nice to have.

    That said, it is often simpler. And that is important. Simpler code is easier to read, easier to modify. Note that I distinguish between simple and easy -- the distinction articulated in the classic talk by Rich Hickey. Those new to the style will often find it more confusing; so too the assembly programmers who abhorred the next generation of language and all their ilk.

    By not defining intermediate variables, by not even specifying arguments that can be inferred, we can significantly improve simplicity.

    It's hard argue that this:

    const foo = (arg) => {
      const qux = baz(arg)
      return bar(qux)
    }
    

    or even this:

    const foo = (arg) => bar(baz(arg))
    

    is simpler than this:

    const foo = compose(bar, baz)
    

    And that's because while all three involve these notions:

    • function declaration
    • function reference

    the second one adds also:

    • argument definition
    • function body
    • function application
    • nesting of function calls

    and the first version has:

    • argument definition
    • function body
    • function application
    • local variable definition
    • local variable assignment
    • the return statement

    while the third one adds only

    • function composition

    If simpler means having fewer notions entwined, the point-free version is simpler, even if it's less familiar to some people.


    In the end, much of this comes down to readability. You spend more time reading your own code more than you do writing it. Anyone else spends a lot more time reading it. If you write code that it is simple and readable, you've made the experience much better for everyone. So where point-free code is more readable, use it.

    But don't find it necessary to remove every point in order to, ahem, prove a point. It's easy to fall into the trap of trying to make everything point-free just because you can. We already know that it's possible; we don't need to see the gory details.