Thanks to visit codestin.com
Credit goes to github.com

Skip to content

invariants for partial application#836

Merged
davidchambers merged 1 commit intoramda:masterfrom
davidchambers:invariants
Mar 2, 2015
Merged

invariants for partial application#836
davidchambers merged 1 commit intoramda:masterfrom
davidchambers:invariants

Conversation

@davidchambers
Copy link
Member

I set out to 🍛 the few functions not yet curried and found myself tackling a larger issue.

I'd like to guarantee consistent handling of arguments to Ramda functions. This pull request defines three invariants:

  1. For every function f where f.length > 0, applying f to no arguments results in a TypeError being thrown. (For the record, I don't like this behaviour. I'd prefer to return a function equivalent to f.)
  2. For every function f where f.length > 0, applying f to R.__ gives a function equivalent to f.
  3. For every function f where f.length > 1, applying f to any value other than R.__ gives a function g with length f.length - 1. All three invariants hold for g and all "descendants" of g.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is necessary because R.add(1)() does not throw, but R.inc() must throw to satisfy the first invariant.

Were we to replace the first invariant with one that states that for any function f where f.length > 0, f() is equivalent to f, we wouldn't need the _curry1 decorator here.

Copy link
Member Author

Choose a reason for hiding this comment

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

@TheLudd
Copy link
Contributor

TheLudd commented Feb 24, 2015

Does this mean that always() and identity() will throw? Is that a good idea?

@davidchambers
Copy link
Member Author

Does this mean that always() and identity() will throw?

Yes. If one wants a function which always returns undefined one should use R.always(undefined). R.identity() is better written as undefined.

@TheLudd
Copy link
Contributor

TheLudd commented Feb 24, 2015

Then I'd consider this a breaking change. From the identity documentation:

Good as a default or placeholder function.

This would no longer be true.

What is the purpose of throwing the error? You said yourself that you didn't like it.

@davidchambers
Copy link
Member Author

I'd consider this a breaking change.

Absolutely.

From the identity documentation:

Good as a default or placeholder function.

This would no longer be true.

I disagree. R.identity makes a very good default function:

> R.sortBy(R.identity, ['foo', 'bar', 'baz'])
['bar', 'baz', 'foo']

In every situation where R.identity is used in place of a more complex function, it will be given an argument.

What is the purpose of throwing the error?

I'll let @buzzdecafe answer that. :)

@TheLudd
Copy link
Contributor

TheLudd commented Feb 24, 2015

Sort by is only one example and we do use identityvery much like that on several places in our projects but "default function" is wider than that for me. To me it certainly implies that it can be called without arguments.

@davidchambers
Copy link
Member Author

What sort of higher-order function would call the provided function with no arguments? I can imagine this being done to trigger side effects or to coordinate control flow (as with Mocha's done callback). These things are not within Ramda's purview, though.

@TheLudd
Copy link
Contributor

TheLudd commented Feb 24, 2015

What sort of higher-order function would call the provided function with no arguments?

Must I use it in a higher order function?

These things are not within Ramda's purview, though

I'm confused. What is the purpose and vision of ramda? This is from an earler readme (v0.8.0). While it is not there anymore, it is what I read when I came to ramda and how I understood this project:

The eweda library was written by the developers of this library, with similar goals. But that one strove more for implementation elegance than for practical capabilities. Ramda is all about giving users real-world tools.

Today in the readme we have:

Functional programming is in good part about immutable objects and side-effect free functions. While Ramda does not enforce this, it enables such style to be as frictionless as possible.

I always saw ramda as different than say fantasy-land or haskell stuff. fantasy-land dictates things such as "there must be a thing like Just(null)" which didn't really make any sense to many of the contributors here but held up mathematically. I feel that the ramda docs say that ramda is not intending to go just as far, but still changes are being made where the purpose seems to be purity and the real world usage is sometimes forgotten. An example of this is the non inclusion of noop and deprecateWarning. They might not make sense from a higher order function "pureview" stance but it is actually useful in many real world places.

I guess I am just saying that I think it is nice to aim for purity, a lot of good comes out of it. My own code has certainly improved by ramda inspired writing but I consider it to be counter productive sometimes to focus to much on purity and not in practicality. And I don't understand the need to throw errors in these cases but lets see what @buzzdecafe has to say...

@buzzdecafe
Copy link
Member

i may be the only one who thinks throwing on zero arguments is correct behavior:

#106 (comment)

however, i do not think I and always should throw. My objection is to calling a curried function with no arguments. That makes no sense. Uncurried function, no problem.

@TheLudd
Copy link
Contributor

TheLudd commented Feb 24, 2015

@buzzdecafe Would that mean that point 1 from @davidchambers should be:

  1. For every function f where f.length > 1 (i.e curried function), applying f to no arguments results in a TypeError being thrown.

IFAIK this is already true.

@buzzdecafe
Copy link
Member

@TheLudd yes, that looks right to me. however, i expect that the counter will be that partially applying one value to a function of length 2 yields a function of length 1, which will then throw on zero arguments. from that POV, i am more sympathetic to not throwing on zero args.

Copy link
Member

Choose a reason for hiding this comment

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

This really does nothing like testing 'any other value than R.__', but unless we get into property-based testing, we can't really do that.

@CrossEye
Copy link
Member

This is going to take a little bit of reflection.

My initial reaction is positive. I like the idea of conforming to straightforward laws, and I like these laws.

I also share @davidchambers' objection to throwing the TypeError. It might be time to revisit that.

But the concern @buzzdecafe expressed recently on another issue about pointlessness is even stronger here. We're taking a hit on performance (no idea yet how big) in order to fulfill certain abstract laws, to get a consistency that sounds of more academic than practical concern. To me the key word there is 'sounds'. I can easily imagine scenarios where these things come back to bite us if they're not done right, and recognize just how hard they would be to work around for a user of the library. Hence a little time for reflection is worthwhile.

I'm less concerned about the issue raised by @TheLudd. always and identity are already unary functions. If a user is using them in place of a perceived missing noop function, I suppose we'd be causing a break; that was certainly never the intent, though, and I'd be willing to document it in a changelog and move on...

unless, and I didn't notice it in the implementation, this sneaks back in the idea that always is the K Combinator (#733) and actually has an enforced arity 2. I don't see any good reason for that, and I would rather stick with always :: a -> (() -> a). But I did promise to listen if you wanted to bring it back up, dc.

@CrossEye
Copy link
Member

@TheLudd:

For far too long I've had on the back burner an almost completed Philosophy of Ramda document. I will try to dust it off soon, complete it, and publish it. In it I talk about what I see by "practical". @buzzdecafe is not happy with that term in our mantra, because to pretty much everyone, "practical" means "what will help me". But I think we can be more specific than that.

Part of how we end up being practical is by being extremely consistent. If you know how everything is going to work, there's no surprises. To this end, I'll end up losing one of my favorite functions (#834) because it doesn't really fit; it mutates user data. For the same reason, we do not include deprecationWarning, even though it could be useful, because it doesn't behave the same as the rest of the library. (It's not referentially transparent.)

@davidchambers' suggestion is one more way to add some consistency to the library. In this case, it has to be balanced against a competing pragmatic concern: we will have to see how it affects performance. But that sort of trade-off is essential. Choosing whether or not to add a noop function is less critical, though. It's easy enough to note that it's not really central to Ramda's mission, it doesn't fit well (all other Ramda functions take parameters and return values), and it's quite easy to implement yourself (R.always(undefined) or function() {}), and simply decide there's no real reason to include it.

I will try to get that document published this weekend if not before.

@davidchambers davidchambers force-pushed the invariants branch 2 times, most recently from 929b075 to 18afbb7 Compare February 25, 2015 17:31
@davidchambers
Copy link
Member Author

@buzzdecafe in #106 (comment):

On curry behavior: Let it fail. If you use it wrong, it's your own damn fault. I can't think of a legit use case where someone will call map()()()(fn)()()(list) so I really don't want to accommodate that. It looks wrong, it feels wrong--it is wrong. I'm with Lonsdorf on this: "I see curry as adhering to fn's in the math sense - take 1 arg & return 1 result. So err!" (my emphasis)

Ramda's curried functions do not "take 1 arg & return 1 result". They take some number of arguments and return one value. If f is a curried function of length 3:

  • applying f to three (or more) arguments gives the result;
  • applying f to two arguments gives a curried function of length 1; and
  • applying f to one argument gives a curried function of length 2.

We can generalize this: applying a curried function f to n arguments where n < f.length gives a curried function of length f.length - n. So, if f is a curried function of length 3:

  • applying f to three (or more) arguments gives the result;
  • applying f to two arguments gives a curried function of length 1;
  • applying f to one argument gives a curried function of length 2; and
  • applying f to no arguments gives a curried function of length 3.

We're currently adding an exception to the f.length - n rule to disallow something we consider pointless. Though f()()()()()()() is of course better written as f, I'm strongly opposed to making an exception to such a fundamental law in order to disallow an operation which would otherwise be entirely consistent with that law.

@buzzdecafe
Copy link
Member

We're currently adding an exception to the f.length - n rule to disallow something we consider pointless.

Pointless, yes -- but more importantly, wrong. The question boils down to whether we want to throw when we have detected something wrong, or let the user persist in his folly. I'm not willing to fight for it. I think @CrossEye is also of the opinion that f() == f is desirable.

curry1 is definitely pointless. If accepting f() == f means we can get rid of curry1 wrapper I'm all for it

@davidchambers
Copy link
Member Author

curry1 is definitely pointless. If accepting f() == f means we can get rid of curry1 wrapper I'm all for it

_curry1 would still be necessary to satisfy the second invariant:

For every function f where f.length > 0, applying f to R.__ gives a function equivalent to f.

We could change the predicate to f.length > 1, but this would prevent us from defining unary functions as partially applied binary functions. R.inc could not be defined as R.add(1), for example, since the result of evaluating R.add(1) is a function which respects R.__ but R.inc as a unary function should not respect R.__. Worst of all, R.inc and R.add(1) would no longer be equivalent.

@buzzdecafe
Copy link
Member

right, i've already registered my discomfort with redundant placeholder, so no need to rehash that here.

@CrossEye
Copy link
Member

I wrote

For far too long I've had on the back burner an almost completed Philosophy of Ramda document. I will try to dust it off soon, complete it, and publish it.

I finally published the damned thing: http://fr.umio.us/the-philosophy-of-ramda/

@buzzdecafe
Copy link
Member

hang on. if we have f() === f then curry(add) should satisfy and inc = add(1) should satisfy, no?

@davidchambers
Copy link
Member Author

I finally published the damned thing

🎉

if we have f() === f then curry(add) should satisfy and inc = add(1) should satisfy, no?

That's right. _curry1 would still need to be applied to vanilla unary functions such as identity in order to support R.__ everywhere.

@buzzdecafe
Copy link
Member

i see. seems wasteful, but i am interested to see where this leads.

@davidchambers
Copy link
Member Author

Where do we stand on this pull request in its current form?

@CrossEye
Copy link
Member

CrossEye commented Mar 1, 2015

🌿

davidchambers added a commit that referenced this pull request Mar 2, 2015
invariants for partial application
@davidchambers davidchambers merged commit 5954b6c into ramda:master Mar 2, 2015
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.

4 participants