Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

number: flip S.sub argument order, and remove S.dec and S.inc #391

Merged
merged 1 commit into from
May 13, 2017

Conversation

davidchambers
Copy link
Member

Problem: Defining subtract N functions is awkward. There are currently two options:

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

> S.map(S.add(-1), [1, 2, 3])
[0, 1, 2]

Furthermore, the intuitive approach is incorrect:

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

Solution: Flip S.sub argument order. The intuitive approach then works as expected:

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

Problem with the solution: It's surprising for S.sub(7, 2) to evaluate to -5 rather than 5.

Solution to the problem with the solution: Make S.sub unary. S.sub(7, 2) is then a type error:

S.sub(7, 2);
// ! TypeError: Function applied to too many arguments
//
//   sub :: FiniteNumber -> (FiniteNumber -> FiniteNumber)
//
//   ‘sub’ expected at most one argument but received two arguments.

One can use S.sub_ in situations in which a binary subtraction function is required.


This pull request also removes S.dec and S.inc. S.dec was included in the library in large part because S.sub(S.__, 1) and S.add(-1) are awkward. S.inc was included for symmetry. In a world in which S.sub(1) defines the subtract 1 function, S.dec does not warrant inclusion, in my view. Do others agree?

/cc @CrossEye, @buzzdecafe

@davidchambers davidchambers mentioned this pull request May 11, 2017
@buzzdecafe
Copy link

You rang?

The broader problem is: Is there a way in JS to handle non-commutative infix operators converted to prefix that preserves readable semantics? IMO that answer is "no", having been on this snipe hunt several times.

Given that my answer to the above is "no", and that the new JS lambda syntax is so compact, my recommendation is not to have infix-to-prefix functions on the API at all. Use a lambda. I'd be happy to see such functions removed from the ramda API.

Sorry if this $0.02 is not much help.

@davidchambers
Copy link
Member Author

Given […] that the new JS lambda syntax is so compact, my recommendation is not to have infix-to-prefix functions on the API at all. Use a lambda.

A lambda works fine for S.sub(1), which can easily be rewritten x => x - 1.

A lambda does not work for S.gte(S.Just(0)), which is not equivalent to x => x >= S.Just(0) due to Ord.

We have no choice but to define S.gte in #388. The only questions are arity and argument order.

@gabejohnson
Copy link
Member

Another option I've been kicking around this morning is to have an infix function (not necessarily by that name).

S.lt(2)(1) // true
S.lt(2, 1) // true
S.infix(2, S.lt, 1) // false

You could even curry it to give the semantics of the current batch of binary functions we've discussed.

S.infix(2, S.lt)(1) // false
// or potentially
S.sectionl(2, S.lt)(1) // false
S.sectionr(2, S.lt)(1) // true

sectionl and sectionr would have the semantics described here.

@buzzdecafe
Copy link

A lambda does not work for S.gte(S.Just(0))

I thought that might run into difficulty deeper into the S weeds. I like the idea of infix or section* functions to make the intent crystal clear. But ultimately you won't get anything as nice as (^ 2) vs ( 2 ^)

@gabejohnson
Copy link
Member

l_(2, lt) and r_(lt, 2)?

@davidchambers
Copy link
Member Author

I thought that might run into difficulty deeper into the S weeds.

What did you mean by this?

ultimately you won't get anything as nice as (^ 2) vs (2 ^)

I started working on a new programming language a few days ago. It will bring together the things I most appreciate about Haskell (curried functions) and Lisp (uniform syntax). Here's a teaser:

> S.map (- 1) [1, 2, 3]
[0, 1, 2]

> S.filter (S.gte (S.Just 0)) [S.Nothing, S.Just -1, S.Just 0, S.Just 1]
[Just 0, Just 1]

This may be possible with the help of a macro:

> S.filter (>= S.Just 0) [S.Nothing, S.Just -1, S.Just 0, S.Just 1]
[Just 0, Just 1]

Another option I've been kicking around this morning is to have an infix function (not necessarily by that name).

Interesting idea, Gabe. S.infix(2, S.lt, 1) reads well to me, but I have difficulty interpreting the partially applied forms.

@gabejohnson
Copy link
Member

gabejohnson commented May 11, 2017

@davidchambers, you could instead have a family of three related functions

S.sub(3, 2) // 1
// infix
S.in_(3, sub, 2) // 1
// left section
S.l_(3, sub)(2) // 1
// right section
S.r_(sub, 3)(2) // -1

The reason I chose in_ instead of i_ is to contrast it with l_. They would be difficult to distinguish from each other visually otherwise.

The benefit of this approach is that you don't need any special cases for associative binary functions. They can just be vanilla, uncurried functions.

@buzzdecafe
Copy link

buzzdecafe commented May 11, 2017

I thought that might run into difficulty deeper into the S weeds.

What did you mean by this?

That simple lambdas would run afoul of the type system in more constrained contexts.

@davidchambers
Copy link
Member Author

The benefit of this approach is that you don't need any special cases for associative binary functions. They can just be vanilla, uncurried functions.

That's nice! I like the switch you made to the r_ argument order. r_(lt, 2) reads well to me. The problem remains that the most intuitive expression, lt(2), does not behave as one might expect.

That simple lambdas would run afoul of the type system in more constrained contexts.

I believe you missed my intended point, @buzzdecafe, which does not concern type checking. Z.lte(S.Just(0), S.Just(1)) is equivalent to S.Just(0)['fantasy-land/lte'](S.Just(1)). Without valueOf shenanigans we can't have <= dispatch to fantasy-land/lte.

@gabejohnson
Copy link
Member

The problem remains that the most intuitive expression, lt(2), does not behave as one might expect.

My thinking is that lt(2) would throw an arity error. The only way to partially apply would be to use either curry2 or one of the section functions.

@davidchambers
Copy link
Member Author

My thinking is that lt(2) would throw an arity error. The only way to partially apply would be to use either curry2 or one of the section functions.

Oh, I see. It's not currently possible to define uncurried functions with sanctuary-def, but we could certainly address this limitation.

Haskell Proposed in #388 Proposed by Gabe
(< y) S.lt(y) S.r_(S.lt, y)
(x <) S.lt_(x) S.l_(S.lt, x)
x < y S.lt_(x, y) S.lt(x, y)

I prefer the middle column to the right column. The downside of the approach proposed in #388 is that we'll end up with lots of _-suffixed functions. This doesn't particularly worry me.

Gabe, are you happy to proceed with the plan set out in #388, or would you prefer to give the l_/r_/in_ option further consideration? I'm happy to give you more time to persuade me. :)

@gabejohnson
Copy link
Member

David, I've revised my proposal again after playing around with Haskell this evening.

Haskell Proposed in #388 Proposed by Me
(< y) S.lt(y) S._r(S.lt, y)
(x <) S.lt_(x) S._l(x, S.lt)
x < y S.lt_(x, y) S._in(x, S.lt, y)
(<) x S.lt_(x) S.lt(x)

I've changed {in, l, r}_ to _{in, l, r} as I found it more visually pleasing.

Also note that S._r has the value on the right and the operator on the left while S._l has the value on the left and operator on the right.

S._in allows for explicit infix forms. All that's required is that the (currently) binary curried functions become unary functions that return unary functions.

Looking at the table, my proposal appears a little clunky. But I rarely fully qualify functions that I import in my own work and am more likely to use them as follow.

const {compose, _in, _l, _r, div, gte, lt, sub, Just, Nothing} = S;

const lt5 = _r(lt, 5);
const _3minus = _l(3, sub);

compose(lt5, _3minus)( 12 / 3 ); // true
_in(Just(12), gte, Nothing); // true

I'd like to see if anyone else has opinions on this before moving on. It's been a point of contention in Sanctuary as well as Ramda for quite some time. It even tripped me up today using lodash/fp 😊

I'm going to reference this comment in #239 and ramda/ramda#1497 to see what others think.

@CrossEye
Copy link

@gabejohnson

I've revised my proposal again after playing around with Haskell this evening.

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

@KiaraGrouwstra
Copy link

@gabejohnson: so the proposed S.lt(x) means "greater or equal to x", right? That still feels a bit surprising. 😕

@gabejohnson
Copy link
Member

@tycho01, I don't know what to tell you. Sanctuary, Ramda and Folktale all currently have that behavior. Vanilla lodash does as well. lodash/fp flips the argument order. fnuc changes the order depending on the number of arguments passed.

Additionally, LISPs prefix everything and have the same operator order (< x y) is equivalent to x + y. Haskell and Purescript do as well when switching to prefix mode.

I think that if the "section" functions are highlighted and well explained before encountering any associative, non-commutative binary functions, things should be fine. That is, as long as there is total consistency.

@gabejohnson
Copy link
Member

@CrossEye go for it. I doubt I'll have time for a few weeks anyway.

//. R.inc('XXX');
//. // => NaN
//. R.add(2, true);
//. // => 3
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this example even more. 🤣

//. //
//. // The value at position 1 is not a member of ‘FiniteNumber’.
//. //
//. // See https://github.com/sanctuary-js/sanctuary-def/tree/v0.11.0#FiniteNumber for information about the sanctuary-def/FiniteNumber type.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having to remember to update this each time we upgrade the sanctuary-def dependency.

@davidchambers
Copy link
Member Author

I'm going to be bold and merge this pull request.

One of the undocumented differences between Ramda and Sanctuary is the system of governance. I believe a benevolent dictatorship with a circle of trusted advisors and open two-way communication with the community is an excellent system of governance for open-source software projects. One advantage of this approach is that difficult decisions can be made without unanimous agreement.

I've been thinking about this problem for a couple of years now. The approach put forward here and in #388 is the first I've found satisfactory during this time. I'll exercise my discretion as benevolent dictator by merging this pull request.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants