Ramda: make gt, gte, lt, lte, etc... right associative

Created on 4 Nov 2015  ·  98Comments  ·  Source: ramda/ramda

actual:

var gtFive = R.gt(5);
gtFive(6); // => false

proposal:

var gtFive = R.gt(5);
gtFive(6); // => true
1.0 discussion

Most helpful comment

Here's what I'm still thinking, based on previous conversation in this thread. I know it was back and forth, but in the end, this is what sounded best to me. The actual names are still flexible, but this is the idea that seems to work best:

| Binary | Unary |
| --- | --- |
| gt(8, 5); //=> true; | isGt(5)(8); //=> true 1 |
| gte(8, 5); //=> true | isGte(5)(8); //=> true 1 |
| lt(5, 8); //=> true; | isLt(8)(5); //=> true 1 |
| lte(5, 8); //=> true | isLte(8)(5); //=> true 1 |
| divideInto(12, 4); //=> 3 1 | divide(4)(12); //=> 3 2 |
| subtractFrom(8, 5); //=> 3 1 | subtract(5)(8); //=> 3 2 |
| moduloOf(24, 7); //=> 3 1 | modulo(7)(24); //=> 3 2 |
| mathModOf(-24, 7); //=> 4 1 | mathMod(7)(-24); //=> 4 2 |
| concatOnto('abc', 'pdq'); //=> 'abcpdq' 1 | concat('pdq')('xyz'); //=> 'xyzpdq' 2 |

1 New function name
2 Substantial behavior change

As to the comment I made before, we could simply do

R.reduce(R.subtractFrom, 12, [1, 2, 3, 4])

which would be equivalent to

12 - 1 - 2 - 3 - 4

I would even consider making the unary functions purely unary. For instance:

subtract :: Number -> (Number -> Number)

But that's not essential, and would require significantly more contemplation.

What do others think? Obviously there would have to be good descriptions, and cross-links between the related functions, but do you agree that this would be about as useful and usable as we could make it?

All 98 comments

:+1:

been down this road before. Under this proposal gte(5)(6) !== gte(5, 6). that is surprising. this is where the placeholders came from, the desire to right-section the non-commutative infix operators.

I'm not suggesting shenanigans. I'm in favour of swapping the order of the arguments. One can easily write x < y if both values are known, so these functions are primarily useful in conjunction with partial application. Let's optimize for partial application (as we do elsewhere in the library).

I'm in favour of swapping the order of the arguments.

Does this argument order hold for any non-commutative infix operator converted to prefix?

gt(5, 10) === true // ?
divide(20, 2) === 0.1 // ?
subtract(10, 9) === -1 // ?

How about concat?

Feels very familiar.

divide and subtract, absolutely!

concat I'm not sure about. I do like the fact that R.reduce(R.concat, '', ['foo', 'bar', 'baz']) evaluates to 'foobarbaz'.

I do like the fact that R.reduce(R.concat, '', ['foo', 'bar', 'baz']) evaluates to 'foobarbaz'.

That's my take on the whole concept. Although I'm very frustrated by gt(5) not feeling correct, enough so that I've gone searching for solutions that lead us down inappropriate paths in the past., equivalences like these seem pretty important. It's not just concat.1

R.reduce(R.subtract, 12, [1, 2, 3, 4])

should be equivalent to

12 - 1 - 2 - 3 - 4

This proposal would replace that with

4 - (3 - ( 2 - (1 - 12)))

which is neither as intuitive nor as understandable.

My biggest difficulty is with the fact that I would want to remain consistent across all our operators. Here, I think are all the current functions that can reasonably be called operators, including a few more questionable ones (merge, range, zip, zipWith), with the non-commutative ones highlighted.

  • add
  • both
  • concat
  • difference
  • differenceWith
  • divide
  • either
  • equals
  • gt
  • gte
  • identical
  • intersection
  • intersectionWith
  • lt
  • lte
  • mathMod
  • max
  • maxBy
  • merge ?
  • min
  • minBy
  • modulo
  • multiply
  • range ?
  • subtract
  • union
  • unionWith
  • zip ?
  • zipWith ?

If we were to change subtract, should we not also change difference? And yet all prior art I can find for this in other libraries or languages, all have the same API order as Ramda currently supplies for this function.

I really would like to solve the underlying problem. I simply don't see how to do that.

So I'm afraid I'm :-1: on this.


1I think I first saw this in John Hugh's seminal paper, Why FP Matters, where he points out that

One way to understand (foldr f a) is as a function that replaces all occurrences of Cons in a list by f, and all occurrences of Nil by a. Taking the list [1, 2, 3] as an example, since this means

Cons 1 (Cons 2 (Cons 3 Nil))

then (foldr (+) 0) converts it into

(+) 1 ((+) 2 ((+) 3 0)) = 6

With infix operators, this is nicer still, essentially replacing commas with operators.

How about providing flipped versions, either suffixed (gt_, lte_, etc), or as a properties (gt._, ...)?

How about providing flipped versions

we had that for a while. dropped in favor of placeholder e,g. var greaterThan5 = R.gt(R.__, 5)

I'd be happy to see those come back and placeholder go away.

How about providing flipped versions, either suffixed (gt_, lte_, etc), or as a properties (gt._, ...)?

People have really not liked the properties version in the past, but I like the suffix. At one point we had divideBy and subtractN, but there was never a good answer to how to extend this to things like gt. The suffixed version would do this unambiguously.

If we go this route, the big win, as @buzzdecafe points out, would be allowing us to drop the placeholder. But I think to do that we might want to add a partialN function to compensate for the lost functionality -- just a function to partially apply argument n. That's a minor detail, though.

I really like this idea!

I'd be happy to see those come back and placeholder go away.

Getting rid of placeholders would certainly be nice.

No matter how the parameters are arranged one use case is going to be awkward. It will either be R.gt(5) or R.zipWith(R.gt, [1,2,3], [3,2,1]).

I think suffixed versions would actually be a nice compromise. Having a suffix _ meaning "slightly different variant" could be useful in other cases as well.

Why do you consider placeholders a bad thing?

I'm almost certain I would miss them were they abolished.

@raine They certainly can be very nice. There was some related discussion in #1363.

To throw another suggestion into the mix, perhaps we could offer them as unary functions that return predicates instead, e.g.:

isGt :: Ord a => a -> (a -> Boolean)
isLt :: Ord a => a -> (a -> Boolean)
// etc.
isGt(5) :: Number -> Boolean

@scott-christopher: We could, but I think that would become more _ad hoc_. The advantage of the suffix is that the same solution that works for gt also works for subtract.

And with the underscore as the suffix it retains something of the feel of placeholders while making me think just a bit about right sections.

@CrossEye You're right, that would only make sense for functions returning boolean values. I could get behind the underscore suffix as somewhat of an analog to a prime.

I have been thinking the same way as @scott-christopher. I would prefer functions isGt and subtractBy. These names are IMO much better than gt_*

* _The suffix _ is a ramda standard that means that the function is slightly different than the original one. It probably means that the function is flipped but could really be anything and you'll need to figure out exactly what by reading the docs._

_It probably means that the function is flipped but could really be anything and you'll need to figure out exactly what by reading the docs._

:laughing:

The advantage of the suffix is that the same solution that works for gt also works for subtract.

Are we really interested in having flipped gt? gt_ would be equal to lt and lt_ would be equal to gt. I.e. they would just be aliases.

Are we really interested in having flipped gt? gt_ would be equal to lt[e] and lt_ would be equal to gt[e]. I.e. they would just be aliases.

that's true. the only thing motivating this discussion is that gt(5) does not mean "greater than five", for that you have to either flip(gt)(5) or gt(__, 5).

Are we really interested in having flipped gt? gt_ would be equal to lt and lt_ would be equal to gt. I.e. they would just be aliases.

In the United States, voting age is 18. People who haven't reached their 18th birthday are not allowed to vote; those who have, are. I would much rather express this as

gte_(18)

than as

lte(18)

which does not even come close to expressing my intent.

That's the reason I would like these.

Yes. I do understand how R.gt and R.lt are very unnatural when partially applied.

But Ramda currently has a no alias policy. Thus it seems worth noting that we'd essentially be creating aliases.

Another idea:

R.isGt = R.lt
R.isLt = R.gt

It reads like plain english when partially applied:

R.isGt(17);

Edit: I just saw that @scott-christopher suggested the exact same names above.

I don't have a serious objection to isGt, isLt, etc. My preference is for the underscore suffix as we could then be entirely consistent, and use it for all non-commutative operator functions. It is slightly less discoverable than isXX, but is not bad, and it certainly looks cleaner to me than lt(__, 5) or flip(lt)(5).

I'll just be glad to do _something_ to clean this up!

Of the list added by @CrossEye above, if we were to go down the path of additional functions I'd propose only the following be included for now:

* isGt
* isGte
* isLt
* isLte
* divideBy
* subtractBy
* mathModBy
* moduloBy

I was tempted to add concat to the list, however unlike those functions it does feel somewhat appropriate to just apply flip to it.

@scott-christopher:

This list feels reasonably correct. (I might throw in difference as well.) But subtractBy doesn't feel particularly appropriate. There's another issue with *By, going back to Issue #65, where we decided to use With and By prefixes in a standard manner. somethingBy would accept a unary function that generated a representative value of each element in the list, such as perhaps a sort key. somethingWith would accept a binary (possibly other polyadic?) function that would accept two elements of the list and use the result of that function in some way. This has been stretched a bit to cover things like zipWith, where the elements are not actually both from the same list. And we have an unfortunate holdover from before we created this convention in useWith. But for the most part, that's how we've been using these suffixes.

My inability to find a good name for a flipped subtract, as well as my own personal inability to come up with isGt, isLte, etc., is one of the reasons that I found subtract_, lt_, etc. attractive. So, while divideBy sounds just right, and the modulo names are all right with By, it would be a shame to abandon our current convention, especially as subractBy really doesn't capture it at all. (The previous incarnation was not great, either: subtractN.)

I don't mind the underscore suffix if it means resolving this.

My only slight concern is that it sets a bit of a precedence for the inclusion of functions with very similar functionality to those that already exist by simply adding an underscore to the end, where previously these would have been suggested as additions to the cookbook. I'm not suggesting that we'll actually start approving them, but it may result in more PRs of that nature. Perhaps that's not such a problem ... ¯_(ツ)_/¯

That's one of the reasons my other suggestion was gt._e. Such a solution could be partially automated, so that all binary functions in the library have that property. I can see why people would prefer a simple suffix though.

Btw, objOf is missing in these lists.

Using a postfix _ certainly has it's benefits. It is easy to apply to all relevant names. There is a nice consistency in it. Users would only have to learn about the convention once and would then understand it's meaning in all circumstances we choose to use it in.

One point though: if _ means "flipped" then why not use "F" instead for its mnemonic value?

I don't think it solves the main problem though: that in the current API the most intuitive thing to do yields the wrong result. Adding a _ postfixed flipped version is not going to solve that! People will still have to know that R.gt(5) is misleading and not what they want even though it seems right.

I think R.isGt comes closer to solving that underlying problem because R.isGt(5) reads _better_ than R.gt(5) and thus guides users toward the right choice.

I'm guessing that to someone who has never used Ramda before this:

R.map(R.isGt(3), [1,2,3,4,5]);

will look more sensible than this:

R.map(R.gt_(3), [1,2,3,4,5]);

To me an API design goal is that users can look at the function names only and reasonably guess which function is appropriate.

With the above goal in mind we could rename R.subtract to R.subtractFrom and then make R.subtract a flipped R.subtractFrom. Again, these names would guide users towards the right choice:

R.map(R.subtract(4), [1,2,3,4]); // [-3,-2,-1,0]
R.map(R.subtractFrom(4), [1,2,3,4]); // [3,2,1,0]

Both of the above seems natural to me.

@paldepind: That sounds quite reasonable. I think it captures almost everything. We still need to address the modulo. modulo(5) _sounds_ the same way subtract(5) or gt(5) does. If we can find a good version of these, I'd be happy at least addressing them this way. Because I agree that isGt(5) reads much better, and is much easier to discover.

Lots of good things mentioned here. I think it would make sense if we can distinguish based on infix operators.

The problem is subtract, gt, concat, etc. are typically associated as infix operators. In the Ramda, curried style where data comes last, R.subtract(12) should subtract 12 from the second argument. The following makes a lot of sense to me:

R.pipe(
  R.add(10),
  R.subtract(2),
  R.divide(4)
)(0)
// 2

If becomes confusing when you write it out R.subtract(12, 10) // -2 but thats because you're used to subtract being an infix operator 12 - 10. So I'd vote for suffixing/prefixing to indicate the infix form. So perhaps R.subtract_(12, 10) // 2.

I think a solid test for the proper, non-infix form, is to think of what is more intuitive while piping... Concat is another example of an infix operator that is currently "backwards" in my opinion (although i never thought of it that way until just now):

R.pipe(
  R.last(2),
  R.concat([5, 6, 7]),
)([1, 2, 3, 4])
// [3, 4, 5, 6, 7]

That makes sense, but concat will add it to the beginning currently...

Here's what I'm still thinking, based on previous conversation in this thread. I know it was back and forth, but in the end, this is what sounded best to me. The actual names are still flexible, but this is the idea that seems to work best:

| Binary | Unary |
| --- | --- |
| gt(8, 5); //=> true; | isGt(5)(8); //=> true 1 |
| gte(8, 5); //=> true | isGte(5)(8); //=> true 1 |
| lt(5, 8); //=> true; | isLt(8)(5); //=> true 1 |
| lte(5, 8); //=> true | isLte(8)(5); //=> true 1 |
| divideInto(12, 4); //=> 3 1 | divide(4)(12); //=> 3 2 |
| subtractFrom(8, 5); //=> 3 1 | subtract(5)(8); //=> 3 2 |
| moduloOf(24, 7); //=> 3 1 | modulo(7)(24); //=> 3 2 |
| mathModOf(-24, 7); //=> 4 1 | mathMod(7)(-24); //=> 4 2 |
| concatOnto('abc', 'pdq'); //=> 'abcpdq' 1 | concat('pdq')('xyz'); //=> 'xyzpdq' 2 |

1 New function name
2 Substantial behavior change

As to the comment I made before, we could simply do

R.reduce(R.subtractFrom, 12, [1, 2, 3, 4])

which would be equivalent to

12 - 1 - 2 - 3 - 4

I would even consider making the unary functions purely unary. For instance:

subtract :: Number -> (Number -> Number)

But that's not essential, and would require significantly more contemplation.

What do others think? Obviously there would have to be good descriptions, and cross-links between the related functions, but do you agree that this would be about as useful and usable as we could make it?

I'm in favour of sticking with what we have now. gt(_, 0) seems fine to me, and even if we add isGt, gt(0) will still trip people.

I think consistency is most important.

I was introduced to Ramda with the premise that all the arguments are "backwards", but its the proper order for function composition. This is mostly true right now -- map and filter work much better this way. But there are many functions that aren't properly "backwards".

R.pipe(
  R.filter(R.lte(2)),
  R.map(R.subtract(2)),
  R.concat([1,2,3])
)([1,2,3]
// expected: [-1, 0, 1, 2, 3]

I like the suggestions given by @CrossEye but I don't think there's any need for to contain both forms and make up semantic names like isGte vs gte. Its not intuitive to me which is which. The sound like the same function. If we had some kind of convention where we have flipped / infix operators like R._gt by leading with a _, I think that would be most understandable.

I think we have several different issues here. Although they can be related, _discoverability_ and _readability_ are not the same problem. This:

R.filter(R.gt(3), [1,2,3,4,5]); //=> [1, 2] // WTF?

fails badly at readability. Given the number of times this issue has come up in various forums, the current solution,

R.filter(R.gt(R.__, 3), [1, 2, 3, 4,5]); //=> [4, 5] 

is failing pretty badly at being discoverable. I'm pretty sure this is true even for people who do know about the placeholder. I've fallen prey to it myself. The trouble is simply that the function _reads_ like what one wants from it. Generally, in Ramda, when the function reads the way you expect it to act, it works that way too.

Once you understand the issue, it's easy enough to employ a placeholder or a flip to get what you want, but the result, with either, but especially with a flip, still suffers from readability problems:

R.filter(R.flip(R.gt)(3), [1, 2, 3, 4,5]); //=> [4, 5] 

Even if you import the R variables into your scope to clean it up, it still feels much more obscure than one would want:

filter(gt(__, 3), [1, 2, 3, 4,5]); //=> [4, 5] 

I don't see any beautiful answers to our problem. It's been discussed to death in this thread and others. So to me, the trick is to make our code both as readable and discoverable as we can. I believe our current solution is neither. No matter what we do, we will have some problems with possible misinterpretations, so to some extent, we will always be left expecting documentation to help with discoverability. But here, a well-named function would be much clearer. If the documentation for gt mentions odd issues with the curried form, a user is still more likely to understand that "see also isGt" would likely help than she would "see also gt_" or "see also _gt".) The result is not only more readable, I think it's also more discoverable.

There's one other possibility that just came to me, would be to use operator symbol string the way nucleotides did:

R.filter(R.gt(3), [1, 2, 3, 4,5]); //=> [4, 5]
//but
R.op['>'](3, 5); //=> false

I haven't really thought this through, and again, I might in doing this expect that such named operators as gt were strictly unary. But it's an interesting concept.

R.op['>'] is an interesting idea.

We needn't use the symbol, though. We could have R.gt and R.op.gt. I could get behind this. I'd really appreciate having access to both R.concat and R.op.concat ("suffix" and "prefix").

We could have R.gt and R.op.gt.

I hadn't considered that. I've been quite opposed to adding any internal namespacing inside Ramda. (Except, I guess, when I had those functions like map.idx -- "A foolish consistency...") But I might be able to get behind this. This would allow us to get left and right sections without any further ado. And as this would only be for binary operators, the implementation could be a simple flip.

Although, as I keep noting, we might want to consider making the right sections strictly unary::

gt :: Ord a => a -> (a -> Boolean)
op.gt:: Ord a => a -> a -> Boolean
gt(5)(10); //=> true, additional parameters to `gt` ignored
op.gt(5, 10) ≍ op.gt(5)(10); //=> false

Namespace for operators? I wish I had suggested that. Oh wait -- I did https://github.com/ramda/ramda/issues/175#issuecomment-54825655

:stuck_out_tongue:

+1 for namespaced flipped version.

As for the order of the "unflipped" version, I still stand by consistency. You like this version:

filter(gt(__, 3), [1, 2, 3, 4,5]);

filter where something is greater than three.

But then why isnt everything like that?

map(__, R.inc)

But then why isnt everything like that?

map(__, R.inc)

I don't understand this question. In the example, the placeholder is there for the first argument, a function a -> b. The second argument should be a functor of some kind.

If map took the array first... then you'd do map(__, R.inc). This is effectivly what going on when you do gt(__, 3)

ah i see, you are illustrating the inconsistency that would arise from changing the behavior of curry for infix operators. ok, carry on then, i'm 100% with you

If map took the array first... then you'd do map(__, R.inc). This is effectivly what going on when you do gt(__, 3)

I'm not sure, but I think this is based on a misapprehension of Ramda's chosen ordering. I believe I've seen you mention somewhere (can't recall where) that Ramda's parameter order is often backwards. I haven't bothered responding to that, as it hasn't seemed important, but it does miss the point.

Ramda's parameter order is different from Underscore's/lodash's version, which, right or wrong, took the OO style of Array.prototype methods, and moved the data into the first parameter. Ramda chooses a different order because it makes more sense when currying. When you curry, the best bet is if the parameters are ordered from the least likely to change to the most likely to change.

It is much more likely to want to build a function that maps a specific function against any number of different lists/functors than one that maps any number of different functions against a fixed list/functor. It's not that the latter is never possible, only that it much less common. Hence,

// map:: Functor f => (a -> b) -> f a -> f b
const squareAll = map(square);

instead of

// map2:: Functor f => f a -> (a -> b) -> f b
const opOnSmallPrimes = map2([2, 3, 5, 7, 11]);

This is backward from Underscore, but how it compares to the native prototype is a more difficult question. It's the same order as in many functional languages, though, so it would be just as correct to say that Underscore is backwards. But neither perspective is very helpful.

The big trouble comes with these non-commutative binary operators. There is a clear contradiction between the best order for currying and the obvious order for infix functions. That's still what this debate is about.

I finished that last one off in a hurry. By "the best order for currying" I mean something fairly specific: a parameter more likely to change should come after one less likely to change.

For a relative handful of functions, this is not clear: the two parameters are about equally likely to change. Pretty much all binary operators are like that. But binary operators have one other feature to dictate their ordering: infix notation. Since subtraction is written as a - b, with the minuend on the left and the subtrahend on the right, this gives us a clear answer to how these should be ordered as a plain binary function: subtract(a, b). The opposite feels really wrong: subtract(7, 4); //=> -3 ???

And then we are stumped, not by the math, not by currying, but by English, where grouped together in a partially applied version like subtract(5), it's extremely hard to read as anything other than an operation that subtracts 5 from whatever is passed to it. That's why this keeps coming up, and why I would really like to find something that works well for us. I feel that most of the time, once you become accustomed to Ramda's basic ideas, there's very little surprising. Right now, the behavior here is too surprising.

When I say the order is "backwards", I really mean it in quotes, as in backwards from what we've been accustomed to before Ramda. As you said it facilitates currying. And thats why all my examples have illustrated how some of these functions dont facilitate currying when you're using them with R.pipe or R.compose.

I personally thing those examples are sufficient. Just as people always think at first: map(f, list) was unexpected, they warm up to the idea and understand why. While subtract(7, 4); //=> -3 ??? is unexpected at first, I think its similar, and you'll eventually come to expect it since its _consistent_ with every other function in Ramda. Not to mention, if you wanted an infix operation, you'd just write 7 - 4.

I thiknk we've made our points. Not much left to say. How about we vote on it?

While subtract(7, 4); //=> -3 ??? is unexpected at first, I think its similar, and , you'll eventually come to expect it since its _consistent_ with every other function in Ramda.

That's what I'm not buying. There is nothing in Ramda that offers guidance here. Subtraction is an operator. The two operands are of equal importance, and equally likely to change. Nothing propels us to choose the anti-infix (?!) order. On the other hand, I might be warming up to the notion that there is nothing inherently wrong with reversing the order for these. Nothing is really compelling that notation either, and doing so would clearly solve this problem.

Not to mention, if you wanted an infix operation, you'd just write 7 - 4.

There are plenty of times when you might want to supply an unknown function to operate on your two operands. In those cases, they have to be pure functions and not simply expressions.

I think we've made our points. Not much left to say. How about we vote on it?

That's not how things work around here. We generally wait for a consensus to emerge among interested parties, or at least among the core contributors. If one does not seem likely, we generally don't make any changes, unless we're in an incredibly broken state, and then we might end up using the majority opinion among the core team. But that's rare.

I just had the same issue, expecting R.filter((x) => x > 2, [1,2,3,4,5]) to be the equivalent of R.filter(R.gt(2), [1,2,3,4,5]).

But by stopping and thinking why I expected that I realized it's because in Haskell: filter (>2) [1,2,3,4,5], is actually flipping the gt (3 > 2 == (>2) 3), but in such a sofisticated way that fells natural and makes sense in a filter. It's also natural to write map (10/) [1,2,3], and you don't even realize it's actually not making it prefixed, only surrounding with parens. All in all, I guess we have to live w/ the language we have...

I guess we have to live w/ the language we have...

On the other hand, there are many very nice, well-behaved languages that transpile to JavaScript ...

the problem of infix-to-prefix conversion for non-commutative functions has haunted this lib for a long time. we've tried a few things, but have run into problems every time. for now, i think the simplest thing to do is to say that when converting infix to prefix the argument order stays the same, irrespective of how that _reads_ (e.g. gt(2) looks like "greater than 2" but is in fact "2 is greater than"), and irrespective of whether the function is commutative.

In other words, punt. :football:

FWIW, I read subtract(7, 4) as "subtract 7 from 4", but only after getting comfortable with currying and partial application.

With reduce(gt(3), [1,2,3,4,5]), would it be too crazy to swap the order of the reducer as well?
That would allow flipped versions of those functions to behave in the expected way, but I'm not sure what other implications it would have.

+1 to @MattMS. I wonder, too, if flipping reducer's signature has any negative impact. Maybe mapAccum, reduceBy, reduceWhile, transduce should be changed due to consistency.

@CrossEye wrote two operands are equally important, but some people already feel unnatural to this.

If beginner's unfamiliarity is problem, it's also problem that my library is strange in the present situation. (Though it's not ramda's problem, but it emphasizes importance of consistency.)
I think this kind of consistency is more important than infix notation consistency.

Flipped version of operators, well, that's out of my concern. It can be added if necessary, but are we having any existing flipped version of function? We already have flip and partialRight.

@MattMS

With reduce(gt(3), [1,2,3,4,5]), would it be too crazy to swap the order of the reducer as well?
That would allow flipped versions of those functions to behave in the expected way, but I'm not sure what other implications it would have.

No, it would not be too crazy. In general when using currying that order is more useful. Fortunately we have reduceRight which has that argument order.

@paldepind are you saying (acc, value) or (value, acc) is better for currying?
My thought is the latter, but mine is a fairly inexperienced opinion.

The docs for reduceRight don't seem to explain the reason for using (value, acc), but I guess it is something to do with it traversing right-to-left.

@MattMS Sorry for the confusion :sweat_smile: I'm saying that what you're suggesting (value, acc) is better for currying. Actually reduceRight was recently changed to that. The discussion leading up the the change is here. Basically that argument order arises "naturally" for reduceRight as I argued in the issue and as @CrossEye mentions above. IMO reduceRight should be used as the "default" reduce function unless one has a specific need for a left fold.

Thanks for the clarification 😄

The examples from #1972 show the flipped reduce* arguments are nice when observing the entire process (value + (value + acc)) but this issue seems to come from observing the specific parts of the process (gt(5)).
My thought is making the latter make sense (by flipping both reduce and gt, subtract, etc) would make things less surprising than focusing on the former.

That said, I'm cautious in my suggestion since my experience has only been in small systems, so flipping reduce arguments (to be (value, acc)) may have issues in larger compositions that I have not encountered.

It's an interesting suggestion about using reduceRight as default too, when considering I often break my Arrays over many lines, which would mean it is reduceBottomToTop. 😛

As an aside:
Reading various issues here has been a great education into the reasoning in functional programming.
It makes me think many issues could be a really useful basis for manual pages, especially with links from the API docs to them.

I'll concede the proposed backward argument order would definitely confuse the heck out of users coming from Lodash.

But the thing is, it would confuse them in the exactly same way as the other functions in Ramda, at which point they'll see the pattern and realize the library has been optimized for partial application.

I'd say confusing Lodash users in a consistent matter would be a good thing. Heck, we'd be doing them a disservice by not confusing them with these functions, as it'll just end up taking longer for them to 'get' Ramda.

all prior art I can find for this in other libraries or languages, all have the same API order as Ramda currently supplies for this function.

And here I'd been under the impression that Ramda was founded with the courage to use a parameter order better than that of Lodash! :P

Those following this issue may also have opinions on sanctuary-js/sanctuary#239.

@tycho01:

I'm not even slightly worried about confusing users coming from Underscore/lodash.

The concern as always is that the partial application, while extremely important, is not the whole picture. And there does not seem to be a great solution.

These would be surprising:

R.subtract(12, 5); //=> -7
R.gt(10, 20); //=> true

Although I like the direction Sanctuary is headed on this, I'm not sure it's right for Ramda. I really don't like decorating function names with punctuation.

But the best solution we've had here is the isGt one, which still leaves the main functions as surprising when partially applied.

Maybe the best alternative is to make these permanently curried, and never allow subtract(a, b), only subtract(a)(b). But that's also very inconsistent with the rest of Ramda.

Arrggh.

@CrossEye: Hmm. Perhaps it's been a matter of how I've come to view and interpret Ramda. I guess I'd somehow come to associate the left hand of an infix operator as analogous to the 'left hand' (i.e. calling object) in method expressions for the purpose of point-free syntax in partial application, and presumed the parameter order to by necessity be dictated by that. In that interpretation, I suppose there wasn't much contention between the wholly/partially applied versions.

So that's just an N=1 interpretation, and I'd be curious how other Ramda users would interpret these functions in their wholly-applied form without prior knowledge on them.

Edit: differently put, I think when one were to get over this 'switched' order for whole application (as I think for the most part we've had to do in getting used to Ramda for the other functions anyway), afaict the other problems just sort of seem to disappear; a > b -> R.gt(b, a) :: arr.map(f) -> R.map(f, arr).

I suppose that'd put me on the far right in the @luizbills camp, feeling we might manage fine with just that one version (as opposed to another isGt or foo_$$___$_$ for that matter). But yeah, it'd definitely be kind of unfortunate in terms of backward compatibility.

I guess I'd somehow come to associate the left hand of an infix operator as analogous to the 'left hand' (i.e. calling object) in method expressions for the purpose of point-free syntax in partial application, and presumed the parameter order to by necessity be dictated by that. In that interpretation, I suppose there wasn't much contention between the wholly/partially applied versions.

I'm confused by this. I feel as though you are suggesting the same thing I would expect and coming to an opposite conclusion.

subtract(7, 3);
// is similar to

```rb
7.subtract(3) # in some Ruby-like syntax.

```js
// which to my mind would definitely have to be
//=> 4
// Are you expecting -4 instead?

Right, the Ruby-style 7.subtract(3) matches my method metaphor. Essentially, where I'm coming from is that my interpretations differ between this traditional imperative method-based context versus this FP-optimized context I'm considering Ramda part of.

Interpretations in the traditional (method-based / imperative) context:

1.inc() // 2
['a','b'].append('c') // ['a', 'b', 'c']
'foo'.replace('o', 'e') // 'fee'
7.subtract(3) // 4

Imagined equivalents in the Ramda context, in this interpretation of the imperative method subject coming last (to allow point-free partial application for elegant function composition purposes):

R.inc(1) // 2
R.append('c', ['a','b']) // ['a', 'b', 'c']
R.replace('o', 'e', 'foo') // 'fee'
R.subtract(3, 7) // 4

I imagine the OP here may have come from a similar train of thought.
Admittedly this Ramda context may not be evident to someone who has only been familiar with the former one. I'd like to think there's a certain consistency to it though, and that this should make it easy to get used to this new context.

Does that help clarify?

Well, it definitely means that we do read it differently.

I would find this really surprising:

R.subtract(3, 7); //=> 4

It might still be our best bet. But that maps very clearly to 3 - 7 in my mind. And R.modulo(15, 6) maps to 15 % 6. While R.gt(12, 5) is slightly more ambiguous, it does associates more closely with 12 > 5 than with the reverse.

Perhaps it's time to resurrect https://github.com/ramda/ramda/issues/1497#issuecomment-195644724.

It sounds like a reasonable compromise, yeah.
Restricting them to unary usage I would consider as surprising compared to the other functions without such limitation. It might also be harder to explicitly express in the Hindley-Milner type notation used in the docs...

I would find this really surprising:
R.subtract(3, 7); //=> 4

But R.map(f, xs) is equivalent to xs.map(f), so would it be inconsistent for R.subtract(3, 7) to equal 7.subtract(3) ?

(this might not be a helpful way of thinking about it since you _can't_ do 7.subtract(3) in JS)

It might also be harder to explicitly express in the Hindley-Milner type notation used in the docs...

We could use parens:

gt :: Ord a => a -> (a -> Boolean)

But R.map(f, xs) is equivalent to xs.map(f), so would it be inconsistent for R.subtract(3, 7) to equal 7.subtract(3) ??

Even if that were true, I think we need to consider readability and discoverability alongside questions of internal consistency.

But I don't think it's true. Technically, of course it's not:

const f = R.inc;
const g = multiply(2);
R.map(f, g)(5); //=> 11
g.map(f)(5); //~~> Error: g.map is not a function

or

const square = (x) => x * x;
const xs = {a: 1, b: 2, c: 3};
R.map(square, xs); //=> {"a": 1, "b": 4, "c": 9}
xs.map(square); //~~> Error: xs.map is not a function

Or even for lists:

const xs = ['1', '2', '3'];
const f = parseInt;
R.map(f, xs); //=> [1, 2, 3]
xs.map(f); //=> [1, NaN, NaN]

But I mean more than this technicality. To me, parameters are ordered in a straightforward manner: a parameter less likely to change comes before one more likely to change. Usually, this simply dictates the parameter order. But in some odd cases, several parameters seem equally likely to change. Binary operators fall into this category. With subtraction, for instance, there is no reason to expect the minuend to change more often than the subtrahend.

These are not the only cases. But others like propEq can make arbitrary decisions that have to be documented but that don't have to match common expectations. There is a normal expectation, I believe, that for a function which is serving in the place of an infix operator, the parameters would appear in that order. The trouble is that the English language used to describe them sounds like the reverse when partially applied. We would have no issue at all with

// foo:: Number -> Number -> Number
const foo = curry((a, b) => b - a)

No one would think twice that foo(3, 7); //=> 4 or that foo(3); //~> (b) => b - 3. If you substitute 'subtract' for 'foo', one of those statements is likely surprising.

The rationale, of course, for the parameters less likely to change to come before those more likely to has to do with making these functions easier to compose through currying. But that is done in the absence of any competing pressure. Generally we can simply make a decision about how to order the parameters to our functions as we choose. (I don't consider the prior art of jQuery, Underscore, lodash, etc. to be competing pressure at all; nor do I think that something like Array.prototype.map has any influence. Those are too far from the style of work Ramda is trying to support.) But for these binary operators, the English language and the common idioms of arithmetic do provide a push in the opposite direction. Here is where I really get language envy. The ability to include left and right sections, the ability to convert between prefix and infix, and other such features makes me want to be working in Haskell.


But, the more we live with the current versions, the more that we need to have these same discussions, the more I think that we might simply be best off ignoring the issue of readability of the fully applied versions and think about how we would like to present these with a flipped order.

As @davidchambers notes, we can simply do

gt :: Ord a => a -> (a -> Number)

and

subtract :: Number -> (Number -> Number)

I know I personally would have fewer issues with this than with the current versions. Although I do sometimes use the fully applied version, I'm much more likely to curry in the one I want, and remembering to add the placeholder, or even reading the code with the placeholder, is harder than it would be if we flipped.

So, @asaf-romano, @buzzdecafe, @davidchambers, @kedashoe, @paldepind, @raine, @scott-christopher, @TheLudd, what so you? Is it time we switch? If so, which functions? All of the list in https://github.com/ramda/ramda/issues/1497#issuecomment-153929590? Or some smaller set?

FWIW, my personal usage makes me think the subtrahend is less likely to change than the minuend.
Same with the right argument in comparison, in value < limit, wouldn't the limit be more likely to be fixed?

Apologies for commenting if your call-outs were indicating this discussion should now be limited to members only.

Apologies for commenting if your call-outs were indicating this discussion should now be limited to members only.

Not in the least; there is a separate channel for that.

I agree entirely. But the tension is between the partially applied and the fully applied versions.

My preference would still be to introduce the following unary functions while deprecating their existing counterparts.

  • isGt
  • isGte
  • isLt
  • isLte
  • divideBy
  • subtractBy (perhaps subtracting?)
  • mathModBy
  • moduloBy

I have a proposal for Sanctuary in https://github.com/sanctuary-js/sanctuary/pull/391#issuecomment-300962716 inspired by Haskell's infix operator sections.

It may also be appropriate for Ramda.

@gabejohnson

It may also be appropriate for Ramda.

As I said there, this is the best proposal I've yet seen in this never-ending debate. I will try to make a PR using this, unless you want to do it.

@CrossEye it's all yours. I'd suggest getting all of the operators you listed in https://github.com/ramda/ramda/issues/1497#issuecomment-153929590 on that same page as well.

Sanctuary now provides this behaviour:

const S = require('sanctuary')

S.gte
// => gte :: Ord a => a -> (a -> Boolean)

S.gte_
// => gte_ :: Ord a => a -> a -> Boolean

S.filter(S.gte(0), [0, -1, 2, -3, 4])
// => [0, 2, 4]

S.gte_(0, 42)
// => false

S.gte(0, 42)
// ! TypeError: Function applied to too many arguments
//
//   gte :: Ord a => a -> (a -> Boolean)
//
//   ‘gte’ expected at most one argument but received two arguments.

as fro me, it should work like this:

R.gt(10, 5); // => true
R.gt(10)(5); // => true

(which is also how it works now)

because as I am thinking about it, the operator is > and it's applied between the 2 arguments (10 > 5). (reason I am thinking about it like that is probably clojure https://clojuredocs.org/clojure.core/%3E where it works exactly this way)

I don't know Clojure, so please excuse me guessing at the syntax.
Does that mean (filter (> 2) [1 2 3 4]) equals [1 2]?

If so then I guess they don't see the partial application as an issue.
But to me it still seems confusing.

@MattMS no. the result is [1] (2 is not less than 2).

Partial application means that you provide not all arguments to the function, which creates function taking rest of the arguments. Right? So the way it currently works:

// 10 > 2
R.gt(10, 2) // => true

const is10GreaterThan = R.gt(10);
is10GreaterThan(2); // => true

so as for me - it works as it should.

note that in Haskell, which is language where all functions are currying, it works exactly the same way.

(>) 10 2 -- => True

let is10GraterThan = (>) 10
is10GreaterThan 2 -- => True

you can think of writing (>) 10 2 in Haskell as writing R.gt(10)(2) in ramda.

see http://learnyouahaskell.com/higher-order-functions for more information on how currying works in Haskell.

To further improve consistency with these function names, I propose we rename isEmpty to isFull. 😃

@nenadalm Regardless of other languages (imho people can always transpile to JS) I think understanding R.gt(10) as is10GreaterThan goes against how RamdaJS works.

I always think of R.<some action>(action parameter) in the way of _"Apply the action specified with the parameter on the subject"_. E.g. R.contains, R.prepend, R.omit... (the "subject" being a JS collection) all work this way.
R.gt(X) reads like "greater than X" with the subject being on the left side of the comparison. Because this is not the case in Ramda today so many people have problems with R.gt(X).

@semmel see #2177 for an alternative.

Although I like the direction Sanctuary is headed on this, I'm not sure it's right for Ramda. I really don't like decorating function names with punctuation.
--@CrossEye

I agree, please stop abusing underscore (the character, not the library). Being new to Ramda but familiar with functional programming, I read _ (and thus__, hard to tell the difference) as _ignore/discard this parameter_ in function signatures. We are talking about function calls here, not function signature, but I still find myself confused. Using the fully qualified name (R.__) at least tell me that something other than discarding is going on.

Add to this the fact that Underscore and Lodash have already abused this character more than jQuery and friends have messed up our legacy code with dollar signs everywhere, leaving the next generation of developers with one giant question: _What does the dollar sign mean in JavaScript?_ Same deal for underscore.

@CrossEye, you mentioned maybe getting rid of the placeholder operator at some point in the future. In my opinion, leaving a trail of R.gt_ (or R._gt), R.lt_, etc. with a naming convention tied to this current Ramda function name is a poor choice because of all of the above.

The use of _ (or in Ramda __) is borrowed from other languages where it's simply a convention for "this parameter does not matter." It's especially useful in pattern matching, but it has a number of uses. Ramda uses it simply to skip a parameter in partial application. I really don't want to see it grow beyond that in Ramda, especially prefix, as there is a large community of JS users who use that to mean "private".

I don't know if we'll ever be able to get rid of the placeholder; it is useful. But if I find myself using it often in my code, I feel like something is really wrong.

Thanks, I forgot to mention the classical language-agnostic convention of underscore-prefixing a field name to indicate private access. Now that we will be seeing actual private fields in JavaScript classes, this will decreasingly be an issue but we still need to avoid confusion if we can. Also, an underscore prefix or suffix generally does not increase readability.

The use of _ (or in Ramda __) is borrowed from other languages where it's simply a convention for "this parameter does not matter."

Yes, this is exactly how I read underscore when used as a parameter.

Ramda uses it simply to skip a parameter in partial application.

I would say that we defer a parameter, since we will still need to apply it later.

I would say that we defer a parameter, since we will still need to apply it later.

Fair enough.

I really think we should swap the argument order of gt, lt, subtract and more where it makes sense before 1.0.0. This issue just came up again on the gitter channel. To this day this is still something that confuses users.

... but as always, at the expense of those who reasonably expect subtract(10, 7) //=> 3

I really, really want left/right sections.

Strict currying is the answer. ;)

I really, really want left/right sections.

@CrossEye see https://github.com/ramda/ramda/issues/2177#issuecomment-302173793

but as always, at the expense of those who reasonably expect subtract(10, 7) //=> 3

The thing is, no one writes that when they can write 10 - 7 instead.

The thing is, no one writes that when they can write 10 - 7 instead.

Or 3 :stuck_out_tongue: .

I see this a lot, but there are valid cases where flipping the order would be damn surprising.

Nobody calls subtract(10, 7) but I would wager that stuff like zipWith(subtract, [10, 9, 8], [3, 2, 1]) gets called on occasion, or reduce(concat) to use an example from another discussion here.

With a binary function, we need to consider that it can be fully applied, partially applied, or passed as a parameter to a higher order function with no arguments applied.

The current argument order favors two out of the three cases, but is really surprising for the third case. Flipping the order would simply change _which_ applications were surprising.

The thing is, no one writes that when they can write 10 - 7 instead.

This may be the case for -, but for other operators Ramda could provide more general functions. S.lte (y) (x), for example, is not equivalent to x <= y because S.lte supports any Ord type.

@gabejohnson: I'd forgotten all about that. I really like that idea. Will respond over there, though.

Came here for this. Adding my grain of salt by voicing in favor of swapping order. I find myself, more often than not, writing gt, lt, etc. on compose chains or as stand alone functions and they always look and read weird.

This is a real life example I encountered today:

const doIfConfirmedGreaterThanZero = fn => when(
 compose(lt(0), prop('confirmed')),  // <- semantics contradict function name
 fn
);

Alternatives look somewhat hacky, IMHO: gt(R.__, 0), flip(gt)(0), partialRight(gt, [0]).

@Bradcomp

Nobody calls subtract(10, 7) but I would wager that stuff like zipWith(subtract, [10, 9, 8], [3, 2, 1]) gets called on occasion, or reduce(concat) to use an example from another discussion here.

That is a very good point.

The current argument order favors two out of the three cases, but is really surprising for the third case. Flipping the order would simply change _which_ applications were surprising.

Good point as well. But, I'd still say that flipping the order would make the _most common_ application the least surprising one.

@CrossEye

That's my take on the whole concept. reduce(concat, '', ['a', 'b', 'c']) // => 'abc'

How about using flip when passing them to higher order functions.
reduce(flip(concat), '', ['a', 'b', 'c']) // => 'abc'

Notice that the majority of people opt for flipping the arguments. I also do think that we should facilitate function composition and use flip when passing them to higher order functions.

@1024gs:

I don't think that's at all possible. We don't know when that's happening. A function doesn't know how and where it's being called. And a function that accepts another function as a parameter does not know that it is a non-commutative operator. I see no way to automate this.

@CrossEye

I see no way to automate this.

I don't think they meant that. I believe they were simply suggesting flipping the argument order as a solution, and asking people to use flip() in cases like reduce(concat).

To throw in my $0.02, Ramda already has so many functions and, postfix or otherwise, I think additional functions would just make things worse overall.

@CrossEye
I did not mean to automate it (sometimes automation is the biggest source of bugs).
I meant to make gt, gte, lt, lte, etc... right associative and in cases like reduce(concat) I would suggest, and I think it is a good suggestion, the user to use flip().
e.g. reduce(flip(concat), '', ['a', 'b', 'c']) would evaluate to 'abc'

Here's your suggestion in action, @1024gs:

> S.map (S.sub (1)) ([1, 2, 3, 4, 5])
[0, 1, 2, 3, 4]

> S.reduce (S.flip (S.sub)) (1000) ([1, 2, 3, 4, 5])
985

I've had no problems with this behaviour. :)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

davidchambers picture davidchambers  ·  39Comments

davidchambers picture davidchambers  ·  50Comments

bobiblazeski picture bobiblazeski  ·  38Comments

Andrew-Webb picture Andrew-Webb  ·  68Comments

hitmands picture hitmands  ·  52Comments