Auto-currying in Rust


#1

I was thinking about how auto-currying would work for Rust. I’m assuming that the back-end can completely optimize cases like foo(a, b) which is really foo(a)(b) without any noticeable performance loss (so just one function call).

Now, auto-currying arguments also affects possible default/optional arguments/overloading. If an optional/default/overloaded argument was absent at the end, it’s not clear whether the function should return a closure or to execute.

The most clear way to avoid this is to make sure that optional arguments always PRECEDE the positional arguments. So the easiest way is to say “put all of the optional arguments first”. But this is weird because self usually comes first in methods.

I can see a few ways to solve this issue if Rust wants automatic currying and partial application.

  1. Put self at the end instead.
  2. Curry backwards instead, starting with the last argument (lol wut)
  3. Weird disambiguation tricks for partial application
  4. Have keyword arguments that don’t come in any particular order except always before positional arguments - something like unwrap(or => "default", foo) vs. unwrap(foo)

Was there any discussion of auto-currying in Rust before that I missed?


#2

Would it be workable if currying and default arguments were mutually exclusive?

some functions seem clear candidates for currying (e.g. operators) and defaults don’t make sense.

(i can see it would be a problem if a library designer adds default args retroactively, but you could just say thats’ the problem of the library designer & its’ users)

doesn’t scala have some syntax specific to currying, that might be another way… you have to explicitly enable it . fn foo(a)(b)© // curryable.


#3

I’d rather have complete automatic currying everywhere.

Then it works like this:

fn unwrap(self) -> T //can fail fn unwrap(self, or => def: T) -> T //can't fail

if you do option::unwrap(or => foo) you get a closure that takes one more parameter and returns foo if it encounters a None if you do option::unwrap(or => foo, bar) it actually executes the closure or you can do option::unwrap(or => foo)(bar) or bar.unwrap(or => foo)

so in this case the function is overloaded with differences purely in keyword arguments option::unwrap refers to a closure that takes a possible keyword argument default or just self if you want the other function if it encounters keyword arguments the closure keeps returning closures, if it encounters positional arguments like self it doesn’t accept any more keyword arguments until the positional argument list is finished, at which point it executes

This way you get auto-currying of every function, a way of overloading with keyword args as long as there are positional args. Unlike smalltalk, these keyword arguments are not order-dependent, since you’re supposed to specify them before positional arguments when you call the function, even if they’re declared at the end.


#4

perhaps currying and OOP syntax is just a can of worms

So you’re suggesting ‘self’ for purposes of currying is actually the last arg… hmm, i think you’re right most of the uses for curing the ‘self’ would probably be the thing you want the caller to supply. Maybe you’d suggest the currying should always work ‘backwards’ ? foo(self,a,b,c) foo(b,c) // closure waiting for self.foo(a) or?

fn unwrap(self, or => def: T) -> T //can't fail

this looks more like “mixfix function names” to me? (like objC?) or even overloading. i.e. unwrap() and unwrap( or=>) are 2 distinct functions?

I think this is separate to the type of default arguments I see, which is completely distinct from overloading.

Perhaps this isn’t a good example for currying and named-params, since in this example you could just write two separate functions unwrap() and unwrap_or() and avoid confusion

My example is fn slice(&self, from:int=0, to:int=self.len()){..};
then you could call x.slice()(get the whole thing), x.slice(from=>30) (all from 30, ‘to=self.len()’),
x.slice(to=>40) (all until item 40 ,‘from=0’),
x.self.slice(2,7); from=2 to=7 … in each case its the exact same function being invoked.

Are you suggesting slice(), slice(0,10) on its own in both cases are just a closure waiting for the ‘self’…

Just setting up debug graphics rendering now, I’m missing C++ defaults. draw_line(start,end, colour:u32=0xfffff); draw_axes(m:&Matrix=identity, colour:u32 =0xffffff, size:f32=1.0f32); draw_axes() - draw the current coord frame draw axes(..some..specific object) draw axes(..some..specific object, random_color(i),0.1) // draw skeleton joints a bit smaller… // don’t want to create & name lots of separate wrapper functions to do the same thing… the parameters make the variation of the call obvious. Its just debug, it wants to be quick and dirty to call with the minimum fuss…

How would you reconcile the curry-ability of these;

I’d be happy to sacrifice the ability for those to curry in exchange for the maximum leverage of default args.

Whilst in these cases I can see great use for having all args defaulted, thats’ complimentary to the use of currying for binary operators (which have no sensible default, or even if they did, the currying is clearly more useful more of the time, and they shouldn’t be members IMO as they don’t belong to one or the other)

maybe a special syntax could always curry ? foo(,…). but if you need to be more specific, you might as well write a closure.

Some languages seem to have a placeholder for a lambda arg as an alternative to currying… maybe that would be another approach ? (swift has $0 $1… ? )


#5

Like in smalltalk, unwrap and unwrap or=> are separate functions. I’m sure my design is accidentally like Obj-C, but I have not looked at it. I’m trying to support both keyword arguments and currying.

fn slice(&self, from:int=0, to:int=self.len()) -> &'a str { //implementation } will look more like:

fn slice(&self, from => from : int, to => to : int) -> &'a str { //implementation } fn slice(&self, from => from : int) -> &'a str { to = self.len(); become self.slice(from, to); } fn slice(&self, to => to : int) -> &'a str { from = 0;  become self.slice(from, to); }

so you can either use the keyword args positionally or as keywords, depending on what’s clearer but for currying you have to use them positionally as:

blah = str::slice(from=>1); //returns a closure that slices from the second character blah(foo); //slices foo from second character to end blah(to=>3); //closure that slices from the second to fourth character blah(to=>3, foo); //slices foo from second to fourth character

or you could reverse the order of application like

blah = str::slice(to=>3)(from=>1)(foo);


#6

I’m sure my design is accidentally like Obj-C, but I have not looked at it. I’m trying to support both keyword arguments and currying.

I think so, i’ve not seen smalltalk, only objc, but I heard objc was inspired by smalltalk.

Maybe they’re right to not support any :slight_smile: … given the conflicting demands, either one scheme will confuse people coming from different regimes. The haskell people all want straightforward currying, the C++ people want trailing defaults, and so on.

fn slice(&self, from => from : int, to => to : int) -> &'a str { //implementation }

… to me this seems like it misses leveraging the names one has already created / or it seems confusing to have to place multiple names in the arglist; whereas just supplying defaults slots into the traditional function call syntax easily - familiar to C++, Python,Scala users already.

Would any of this also be easier if they’d started with UFCS?

Maybe one way around it would be to say “only free functions can be curried”? then you write a wrapper fn slice<T:Slice>(from:int,to:int,self:T){ self.slice(from,to) } if you want to enable currying and have the ‘self’ as the missing arg.

Ok… I can see you’re basically saying that curryable wrapper above should be available for all methods by default. but maybe starting with free functions would have less to go wrong, and give more chance for people to explore the issues


#7

You forget that default arguments don’t support all use-cases.

Let’s say you want unwrap() and unwrap(or=>def) to have the same function name, but in the unwrap version you don’t have a default argument at all. How are you going to support this overloading when you have default arguments? The default is None? Then you have to wrap the def arg in Some. That’s awful.

In dynamic languages that don’t care about performance you’d just say if(!def) and be done with it. In Rust you can’t refer to def because it wasn’t even passed in at all!


#8

What would be the purpose of having a unwrap() if you have a unwrap(or=>def)? How would the compiler even figure out which function to call?


#9

as I explained, the way that the compiler figures it out is that you have an implied self in a method call, but if you don’t specify it you can just go ahead

blah.unwrap() is just unwrap() called on blah option::unwrap() is a closure that is waiting for something to unwrap immediately option::unwrap(or => default_thing) is a closure waiting for something to unwrap with default_thing as the default option::unwrap(or => default_thing, blah) gets the last positional argument and executes

I mean, you may not want this particular function to have a keyword argument (I don’t see why not, foo.unwrap(or => bar) seems pretty clear), but the point is this can be allowed as long as all keyword arguments come before positional arguments

The ambiguity is resolved, you get currying, overloading and keyword arguments. Those features are not actually mutually exclusive.


#10

We are talking about completely different features for completely different problems. They are complimentary. Your proposal doesn’t help the cases I’m talking about, and vica versa.

You are talking about sugar for naming different functions, for different functions

I am talking about sugar that avoids the need for multiple wrapper functions, where you want the same function with several tiny variations.

e.g.

currently in Rust we need separate functions “as_slice(), slice_from(x), slice_to(x), slice(from,to)” …

fn slice(&self,from:uint,to:uint) { .. the meat.. } fn slice_from(&self,from:uint) { self.slice(from, self.len()) } fn slice_to(&self,to:uint) { self.slice(0,to) } fn as_slice(&self,from:uint, to:uint) { self.slice(from,to); } … notice how many times ‘slice,from,to’ are repeated.

With Default+Keyword Args, you’d replace all 4 functions with one…`

fn slice(&self, from:uint=0, to:uint=self.len()){.. the meat..};

Now you would just omit or name parameters to get … foo.slice() same as self.as_slice() was. from,to omitted so take default foo.slice(to=10) same as foo.slice_to(10) was'. name arg 'to',skip defaulted'from' foo.slice(2,5) same as foo.slice(2,5) was foo.slice(5) same as foo.slice_from(5) was, to=self.len(). ‘5’ is positional arg ‘from’

It becomes trivial to add these conveniences. My proposal - has precent in many languages - better leverages the code and names you create to avoid a certain set of trivial wrappers.

It reduces navigation time because you have less names to search through, and less definitions to jump through. Less symbols in the autocomplete.

Similarly my draw_axes(matrix), draw_axes_color(matrix, colour) draw_axes_color_size(matrix,colour,size) … In C++ these could just be one function void draw_axes(matrix,u32 color=0xffffffff,f32 size=1.0f);

How are you going to support this overloading when you have default arguments?

C++ has overloading AND default arguments , and it works. These are different features

void draw_axes(Matrix& m, u32 color=0xffffffff, float size=1.0f) void draw_axes(ColumnMatrix& m, u32 color=0xffffffff, float size=1.0f)

… You write as many variations as you want, with different numbers and types of args so long as its unambiguous…

The default is None? Then you have to wrap the def arg in Some.

No, I never suggested that. That is a (terrible) alternative idea people use to argue AGAINST the feature I’m after. Passing an optional argument as an option is more verbose and adds complexity to the routine. I’m talking about leveraging simple cases, where a simple push of a value avoids needing a whole extra function or piece of logic.

my C++ solution for unwrap_or() would also be different:-

in C++ that is an overload - selecting a different function by passing a different number of args - not a default parameter on the same function. It could look like this…

template<class T> class Option { bool is_some; T value; ... T unwrap() {assert(this->is_some); return this->value; } T unwrap(T default) { if (this->is_some) return this->value; else return default;}}`


#11

This might be a side note that was already considered somewhere, but: What I fear for auto-currying is that if you forget an argument, the method/function will just not run. Example:

fn log_something(x: int, y: int) { println!("log {}", x + y) }
...
log_something(23);

It looks like it does something, but doesn’t.


#12

Perhaps the compiler could signal an unused curry/function/variable warning or error in this case? It certainly seems easy enough for it to detect this as a logical no-op, as I don’t think anyone is proposing partial application should ever have side-effects.


#13

Well, it would probably end up being attached to the type, like with Result<T>. I don’t plan on using the feature, I just wanted to note something that would trip me up.


#14

I understand that this is what you want, but what you want is pure syntactic sugar for some cases. The rule for your case is “default arguments should come before positional arguments”.

so maybe something like:

fn slice(&self, from => from:uint = 0, to => to:uint = self.len()) -> &'a str { //stuff }

so your use case is a subset of my use case and is actually just sugar. The fact that I mark it as a keyword argument lets the compiler know it’s not a positional argument.


#15

Most of the time you’d be using methods like

log.warning(x, y)

using the method syntax will always execute because it will pass in the self argument as the positional argument and the function will execute

only static methods and free functions can have this kind of arity mismatch


#16

“default arguments should come before positional arguments”.

What python allows now… (i think scala copies this too)

def bar(a,b,c): print a,b,c

def foo(a=1,b=2,c=3): print a,b,c   # all these arguments are
                                    # positional,defaulted, 
                                    #and optionally accessible by name ,eg...

foo()   ==>  "1,2,3"
foo (10)  ==>  "10,2,3"
foo (c=30) ==> "1,2,30"
foo(10,20,30) ==> "10,20,30"
foo(c=5,a=5) ==>  "5,2,5"

bar(c=10,b=5,a=1) ==> “1,5,10” #no defaults but you can still name the args bar(1,2,3) ==> “1,2,3”

I think this is simple and intuitive, and powerful for APIs: it maximises the value of each symbol you created. It has no distinction between positional & keyword args… all args can be specified by keywords, mostly to achieve the ability to freely decide what is specified or omitted… e.g. if you want to omit the middle argument. (its not about functions with lots of args, its auto-sugaring calls with just 2,3,4 args aswell)

and you always get to annotate meaning of specific args using their name if you want to


#17

Python doesn’t have auto-currying. You need at least one positional argument to end the currying and execute. This is what OCaml does which does support optional keyword arguments and automatic currying. Otherwise all of your functions will look like foo(a=>1)() //second set of parentheses to actually execute


#18

thats’ why i’d be happy with it ‘either/or’ … In some situations i’d want to fully leverage defaults/keywords. In others… currying is more clearly beneficial.

i can see why they’re doing neither, its not clear which more people would go for. Rust already has a nice lambda syntax… its 4 characters in the case of a single argument.

i think its hard to reconcile currying and OOP syntax. I realise there’s logic and reasoning behind what you’re proposing (marking out the keyword names… and the ‘self’ being the a positional arg that you usually want last… ) … but I would bet that this is all harder to learn than what python does.

mind you people even complain about ‘complexity’ when we suggest copying pythons defaults :frowning: … which at least to my mind fit better with imperative+method syntax.


#19

Another solution, instead of trying to mess up with partial application, OOP and optional arguments, is to have a wildcard like foo(_,2,3,_) that would partially apply the function, except where there are wildcards. No issue with default arguments and it’s a bit nicer for positional stuff.

Default arguments and partial application in ocaml mix very badly together and is the source of various surprising and unintuitive behaviors, I wouldn’t advise to try repeating the same mistakes.

Bonus point: it opens the door for a shortcut syntax for closures: (_ + 3) for |x| (x + 3) (maybe not the best example, because type inference, but you get the idea).


#20

Yes, that’s good, but I like keywords more because the intent is clearer.

addNewControl("New", 50, 20, 100, 50);

What does that mean? Types don’t help, it’s just &str, int, int, int, int

how about this instead?

addNewControl(title => "New",
          y => 50,
          x => 20,
          width => 100,
          height => 50);

much clearer and you can see how it’s easy to curry or make some parameters optional

with your proposal you could do addNewControl(_, 50, _, 100, _); which is again, not very clear and easy to mess up (wrong arguments in the wrong slots)