Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upCurrying RFC #191
Conversation
huonw
reviewed
Aug 5, 2014
| You get something like partial application when all functions are curried. | ||
| Thus, you can write `fn add(a: int, b: int) -> int { a + b }` and call it with `add(1)` | ||
| and get `|b: int| -> (b + 1)` back. This means you can get closures easily without | ||
| having heavy closure return types. This pattern allows you to code without having all |
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
There's no need for 'heavy closure return types', e.g. just writing |b| add(1, b) works.
This comment has been minimized.
This comment has been minimized.
iopq
Aug 5, 2014
Author
Contributor
if I return |b| add(1, b) from my function I will have to declare the function's return type to be -> |int| -> int
if add is implemented that its return type is Vec<Rc<Cell<int>>> then the return type of the function when curried is |Vec<Rc<Cell<int>>>| -> Vec<Rc<Cell<int>>>
This comment has been minimized.
This comment has been minimized.
huonw
Aug 6, 2014
Member
I think there's basically two reasons one would explicitly return a closure:
- to make currying easier
- to be able to cache some work that can be reused repeatedly later
The first is (slightly verbosely) replaced by writing |b| add(1, b) at the call site, and automatic currying is not really a replacement for the second.
To replace 2, the compiler would have to do some form of partial evaluation/partial application, which I imagine would be quite difficult to do in a reliable manner in Rust, i.e. if it actually mattered for performance a human would probably do the partial application transformation manually anyway.
huonw
reviewed
Aug 5, 2014
| # Detailed design | ||
|
|
||
| A function called with no arguments remaining in its signature is called immediately. | ||
| A function called with some arguments remaining is turned into a closure. |
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
This isn't very detailed. It should at least vaguely the discuss the mechanics of 'turned into a closure', I would imagine that unboxed closures with by-value captures come into play and are worth mentioning, as would exhibiting some code transformations into explicit closure form, e.g. foo(a, b)(c, d); becoming (|x, y| foo(a, b, x, y))(c, d).
Also, what about efficiently handling static vs. dynamic function calls? i.e. let f: Box<FnTrait> = if ... { some_2_arg_closure } else { some_other_2_arg_closure }; let g = f(1); g(2) would presumably need to capture f into the g closure somehow, but would it capture by-value or by-reference? (Sometimes it will need to be by-value, e.g. when currying a FnOnce closure, sometimes it will need to be by-reference, since it is possible to continuing using f later.)
But, then, it would be inefficient (at compile time) to actually capture the function into a closure for static function calls (like calling add(1, 2) above); this would bloat our IR significantly and slow LLVM to a creep (this is an implementation detail but it'd be nice to mention it, since compile times are very user-visible).
Lastly, what about more interesting curries:
- methods
- overloaded call operator (this definitely needs discussing since it is how unboxed closures work)
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
Also, what does foo() do? For uniformity, it 'should' just return a closure around foo, since, if foo takes n args, it's passing 0 out of n, and thus should return a closure taking n args itself. The worst part is when n == 0.
This comment has been minimized.
This comment has been minimized.
iopq
Aug 5, 2014
Author
Contributor
No, because when you reach the 0 argument case it just executes. 0 is the base case, you execute until you reach it. As I understand it, in Haskell getChar doesn't curry just because it has no arguments. It just executes and reads from standard in.
If I write main = getChar it won't ask for more arguments. It will just execute getChar.
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
getChar is not a zero-argument function, (from the outside) it's just a value representing an IO action, that can be executed by being sequenced in main.
Anyway, as I said above 0 is not "logically" the base case, especially not for a closure taking at least one argument, e.g. let f = |a| { ... }; f().
huonw
reviewed
Aug 5, 2014
| A function called with some arguments remaining is turned into a closure. | ||
| `a -> b -> c` is right-associative, equivalent to `a -> (b -> c)` | ||
|
|
||
| # Drawbacks |
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
Another drawback is likely making error messages slightly more confusing, e.g. missing an argument now is:
fn add(x: int, y: int) -> int { x + y }
fn main() {
let z = add(1) * 2; // error: this function takes 2 parameters but 1 parameter was supplied
println!("{}", z);
}but under this RFC it would (naively) become something like "type |int| -> int does not implement Mul". (This should either be a drawback or a plan to avoid these problems should be mentioned above. Investigating how e.g. Haskell handles might be a good idea.)
This comment has been minimized.
This comment has been minimized.
dobkeratops
Aug 5, 2014
Another drawback is likely making error messages slightly more confusing,
^ could the context be used to guess a bit: if you're passing an expression to a function argument, assume the user wants to curry. Anywhere else, give a warning about missing parameters first "add takes 2 parameters; curried to |int|->int"
This comment has been minimized.
This comment has been minimized.
huonw
Aug 5, 2014
Member
Yes, we could infer/guess; but it needs to be written down, because it's very important.
This comment has been minimized.
This comment has been minimized.
|
-1. Explicit currying would be nice (either through a macro or custom syntax), but I think that implicit currying is too much magic. |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Aug 5, 2014
|
how about allowing arity-based overloads ,enabling the community to implement currying or default parameter macros , and see how it fits in the language in widespread use |
This comment has been minimized.
This comment has been minimized.
|
I too would like to see currying done explicitly ( First, because explicit is better than implicit (and simple is better than complex), and second, because then it would be almost orthogonal to parameters being named and/or optional. In my ideal world, it would be possible to say |
This comment has been minimized.
This comment has been minimized.
|
-1 as well. I like the idea of explicit currying though. |
This comment has been minimized.
This comment has been minimized.
|
prefer explicit —Reply to this email directly or view it on GitHub. |
This comment has been minimized.
This comment has been minimized.
|
I'm obviously biased, but I'll just point out that I think the way this RFC handles call-sites would conflict with my proposal for auto-tupling at call-sites: http://discuss.rust-lang.org/t/pre-rfc-auto-tupling-at-call-sites/175 (although maybe a sufficiently clever programmer could use the auto-tupling proposal to implement calls to a curried function.) |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Aug 6, 2014
|
suggestion for the OP: maybe separate the discussion of keyword arguments and defaults - mention in 'alternatives' or 'drawbacks' the option of currying being mutually exclusive with 'trailing defaults', also the suggestion of explicit sugar to disambiguate either. also r.e. 'functions vs methods' - you could suggest only currying free-functions initially, to avoid issues about what people expect 'self' should do. One could always wrap a free function around some method-dispatch
could it still co-exist - this auto-tippling is what to do when you supply more arguments, whereas currying (and even defaults) deal with passing fewer arguments. You could simply say that the variadic arguments are never curried. |
This comment has been minimized.
This comment has been minimized.
|
FWIW, it should be possible to write a |
This comment has been minimized.
This comment has been minimized.
bgamari
commented
Aug 6, 2014
|
@kballard in my opinion you the succinctness of Scala's explicit currying is its primary draw. Introducing a curried call with a macro negates most of this advantage. |
This comment has been minimized.
This comment has been minimized.
dobkeratops
commented
Aug 7, 2014
+1. as soon as you wrap it in a macro.. you might aswell just write a closure.. its just a prefix |x,y| vs bind!(...) the closure might even be fewer characters already |
This comment has been minimized.
This comment has been minimized.
|
The macro approach has the benefit of letting you reorder arguments. I like the |
This comment has been minimized.
This comment has been minimized.
Drup
commented
Aug 8, 2014
|
The nice thing about the syntax |
This comment has been minimized.
This comment has been minimized.
|
Thank you for the suggestion. During the triage meeting today, we decided to close this RFC. As @huonw points out, there are numerous issues that would have to be addressed to flesh out the proposal, but even more than that I think there is a general feeling that automatic currying is not something we plan to add to Rust. |
iopq commentedAug 5, 2014
Automatic currying of all functions and closures