7

When I tried to write JavaScript in pointfree style, I found that if you forced every function in this style, you sometimes lost its readabilty. For example:

import R from 'ramda'

const ceil = Math.ceil

const pagination = {
  total: 101,
  itemsPerPage: 10,
  currentPage: 1
}

// ================= Pointful style ==================
const pageCount = (pagination) => {
  const pages = ceil(pagination.total / pagination.itemsPerPage)
  const remainPages = pagination.total % pagination.itemsPerPage === 0 ? 0 : 1
  return pages + remainPages
} 

pageCount(pagination) // => 11

// ================ Pointfree style ==================
const getPages = R.pipe(
  R.converge(R.divide, [R.prop('total'), R.prop('itemsPerPage')]),
  ceil
)

const getRemainPages = R.ifElse(
  R.pipe(
     R.converge(R.modulo, [R.prop('total'), R.prop('itemsPerPage')]),
     R.equals(0)
  ),
  R.always(0),
  R.always(1)
)

const pageCount2 = R.converge(R.add, [
  getPages,
  getRemainPages
])

pageCount2(pagination) // => 11

I wrote a simple pagination module to calculate the pageCount of giving total items count and items count per page in pointful style and pointfree style. Apparently the pointful style is much more readable than the pointfree style version. The latter is kind of obscure.

Am I doing it right? Is there any way to make the code in pointfree style more readable?

Jerry
  • 909
  • 7
  • 24
  • 4
    Thats why its called "pointless" - the lack of argument naming makes it obscure. – max Feb 06 '17 at 12:59
  • 1
    I would say your code is fine from a scholastic approach - but as actual application code its pretty heavy handed. "The key idea in tacit programming is to assist in operating at the appropriate level of abstraction." - I don't think using two function calls to achieve the simple addition of two integers is the appropriate level of abstraction. – max Feb 06 '17 at 13:18
  • 1
    @max That's why I get confused. Are any simple rules that can tell which kind of scenarios in which you should use pointfree style and which you shouldn't. – Jerry Feb 06 '17 at 13:26
  • 1
    The functional style describes what should be achieved and not how. The algorithm is hidden behind declarative code. This is actually a good feature, because all the details are abstracted away and you can focus on how to stick those small, specialized functions together. You just need to get used to it and to name your composed functions properly. –  Feb 06 '17 at 13:31
  • It doesn't rebut @davidchamber's point below, but your code could be simpler, with the points-free version, `lift(compose(Math.ceil, divide))(prop('total'), prop('itemsPerPage'))`, or the -- again simpler -- pointed `pgn => Math.ceil(pgn.total / pgn.itemsPerPage)`. – Scott Sauyet Feb 06 '17 at 16:27
  • https://wiki.haskell.org/Pointfree#Problems_with_pointfree – Bergi Dec 09 '17 at 23:54

3 Answers3

10

Manual composition

Let's start with manually composing functions:

const calcPages = (totalItems, itemsPerPage) =>
 ceil(div(totalItems, itemsPerPage));


const div = (x, y) => x / y;

const ceil = Math.ceil;


const pagination = {
  total: 101,
  itemsPerPage: 10,
  currentPage: 1
}


console.log(
  calcPages(pagination.total, pagination.itemsPerPage)
);

Programmatic composition

With the next step we abstract the parameters away:

const comp2 = (f, g) => (x, y) => f(g(x, y));

const div = (x, y) => x / y;

const ceil = Math.ceil;


const calcPages = comp2(ceil, div);


const pagination = {
  total: 101,
  itemsPerPage: 10,
  currentPage: 1
}


console.log(
  calcPages(pagination.total, pagination.itemsPerPage)
);

The function definition is now point-free. But the calling code isn't. Provided you know how the higher order function comp2 works, the expression comp2(ceil, div) is pretty declarative for you.

It is now obvious, that calcPages is the wrong name, because the function composition is much more general. Let's call it ... intDiv (well, there is probably a better name, but I suck at math).

Destructuring Modifier

In the next step we modify intDiv so that it can handle objects:

const destruct2 = (x, y) => f => ({[x]:a, [y]:b}) => f(a, b);

const comp2 = (f, g) => (x, y) => f(g(x, y));

const div = (x, y) => x / y;

const ceil = Math.ceil;


const intDiv = comp2(ceil, div);

const calcPages = destruct2("total", "itemsPerPage") (intDiv);


const pagination = {
  total: 101,
  itemsPerPage: 10,
  currentPage: 1
}


console.log(
  calcPages(pagination)
);

I called the modified function calcPages again, because it now expects a specific pagination object and thus is less general.

Provided you know how the involved higher order functions work, everything is declarative and well readable, even though it is written in point-free style.

Conclusion

Point-free style is the result of function composition, currying and higher order functions. It is not a thing in itself. If you stop using these tools in order to avoid point-free style, then you lose a lot of expressiveness and elegance that functional programming provides.

4

Let's start with a simple example:

//    inc :: Number -> Number
const inc = R.add(1);

I find the above clearer than its "pointful" equivalent:

//    inc :: Number -> Number
const inc = n => R.add(1)(n);

The lambda in the line above is just noise once one is comfortable with partially applying Ramda functions.

Let's go to the other extreme:

//    y :: Number
const y = R.ifElse(R.lt(R.__, 0), R.always(0), Math.sqrt)(x);

This would be much more clearly written in "pointful" style:

//    y :: Number
const y = x < 0 ? 0 : Math.sqrt(x);

My suggestion is to use point-free expressions in the simple cases, and revert to "pointful" style when an expression becomes convoluted. I quite often go too far, then undo my last several changes to get back to a clearer expression.

davidchambers
  • 23,918
  • 16
  • 76
  • 105
  • Agreed. And that "other extreme" is nothing near as extreme as I've sometimes seen (or done!) – Scott Sauyet Feb 06 '17 at 16:14
  • 1
    The point-free style is not a thing in itself. It is the consequence of composition and currying. When you compose functions or partially apply curried functions, then your code is automatically point-free. Why should someone implement `if/else` statements as curried functions in the first place? The ternary op is already an expression. So at most to have lazy conditions. Hence not point-free is the problem in your example, but the misuse of curried functions, which are not the proper tool to express imperative conditions. –  Feb 06 '17 at 16:49
  • http://stackoverflow.com/a/42044869/312785 is possibly a better example, @ftor. Do you think I should edit my answer? – davidchambers Feb 06 '17 at 17:30
  • I think the answer to which you refer doesn't support your point of view any better. It is mainly about an iterative transformation and static (object literal) object definition vs. dynamic object construction. Anyway, I guess that there is a consensus at Ramda that Point-free style often leads to poorly readable code. I just think that in most cases misusing function composition/currying is the cause of poorly readable code. It is all opinion-based. –  Feb 06 '17 at 19:13
  • 1
    @davidchambers Should the pointful equivalent be `const inc = (n) => n + 1` in the first example? If so, the pointful style is also more readable than the pointfree style even in simple cases. – Jerry Feb 07 '17 at 02:43
  • 1
    It's useful to ignore the existence of the `+` operator for a more direct comparison, @Jerry. A better comparison might be between `const sum = R.reduce(R.add)(0);` and `const sum = xs => R.reduce(R.add)(0)(xs);`. Any time we have `x => f(x)` we have unnecessary function wrapping; we should simply write `f` instead. – davidchambers Feb 07 '17 at 12:31
2

In my opinion, the problem here is the "readability", more specifically the fact, that you cannot read the code continuously from left to right as a text from a book.

Some functions in the ramda library can be read from right to left, like compose():

import { compose, add, multiply } from 'ramda'

// the flow of the function is from right to left
const fn = compose(add(10), multiply) // (a, b) => (a * b) + 10

Then you get to a point, where certain functions require you to give their parameters in a certain order (non-commutative functions), often in a left to right fashion, like lt().

import { __, lt } from 'ramda'

// you need to read the parameters from left to right to understand it's meaning
const isNegative = lt(__, 0) // n => n < 0

When these direction changes meet up, then you have a harder time reading the code, because it takes more effort to find the path, where the code flows.

import { compose, length, gte, __ } from 'ramda'

// 1) you start reading the code from left to right
// 2) you see compose: switch modes to right to left, jump to the end of compose
// 3) you get to gte, where you need to switch back to left to right mode
// 4) glue all previous parts together in your mind, hope you'll still remember where you started
const hasAtLeast3Items = compose(gte(__, 3), length) // arr => arr.length >= 3

There are a few functions, which are really useful, when working with ramda, but can ruin the readability instantly. Those require you to switch reading directions too many times, requiring you to keep track of too many substeps from the code. The number one function for me from that list is converge.

Note: it might seem like the above problem only occures with functions with more, than 1 parameters, but add() doesn't have this issue, add(__, 3) and add(3) are the same.

Lajos Mészáros
  • 3,756
  • 2
  • 20
  • 26