Auto-currying in Rust

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?

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.

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;}}`

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.

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.

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.

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.

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

"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

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

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.

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).

6 Likes

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)

What you really want here is labels.

How to do labels and how to do wildcards/partial application are slightly orthogonal decisions. The first help the later, but can be designed separately. Of course the two should be compatible.

Also, not sure why you want => instead of the simpler and clearer =.

addNewControl("New", 50, 20, 100, 50); What does that mean? Types don't help, it's just &str, int, int, int, int

agree that the named arguments are a nice solution here (which is why i like the python idea, you get the ability to annotate with names routinely)... .. but with the C++ overloading, you can "say it with types". How about..

addNewControl("New", Point2d(50,20), Size2d(100,50))

'addNewControl' could have all sorts of overloads, - e.g. omit the 'size', and you expect it to figure it out itself. Omit position, and you expect it to place somewhere sensible in the parent window.

Its funny how divisive this is. I've seen people argue against keyword args claiming that leveraging types is superior. (You're creating more semantics the compiler understands) - but I don't see them as mutually exclusive. I'd like both

Also, not sure why you want => instead of the simpler and clearer =.

that doesn't fit in rusts' grammar already; = can be used within expressions. (but it returns () unlike c++.. not sure of the use)

You’re right, they’re orthogonal, but I like my syntax. I think I’ll write up an RFC for just currying. Or is there one already?

addNewControl("New", Point2d(50,20), Size2d(100,50))

This is OK, but you still have to remember that’s it’s title, position, size

If you write addNewControl(Point2d(50,20), "New", Size2d(100,50)) it won’t compile, but with keyword args like

addNewControl(position => Point2d(50,20), title => "New", size => Size2d(100,50))

there is no problem

Apologies if I’m late to the game here, but…

Scala has a solution, whereby you chain parantheses:

def modN(n: Int)(x: Int) = ((x % n) == 0)

In Rust it might look something like:

fn map(f)(xs) { blah }

With that syntax, each grouping of parameters in () could contain its own mix of optional, keyword and positional arguments.

The syntax also maps neatly onto how you’d then expect to call the function.

1 Like

I believe I’m also a bit late, but I’m glad that this thread is still alive.

I just wanted to say that I really like @Drup’s idea about wildcards/placeholders foo(_, 2, _, 3) for a few reasons:

  • The original function doesn’t need to be modified in order to allow this
  • As he suggests, it could also be used with operators and I believe that more complex expressions too, although I’m not sure to what degree it could be nested without ambiguity (for example, in _ + _.f(1, _) is the third “_” the identity function or the third parameter of the entire expression?)
  • There’s no syntactic noise at all

I have one doubt though. What if the arguments aren’t exactly in that order? Could it be possible to use ordered placeholders to get something like $2.f($1)? I guess that repeating the dollar or whatever character is prefered could be used to escape nested expressions and avoid the ambiguity problem I mentioned in the second bullet: $1 + $2.f(1, $$1). I’m not sure that this fixes all the cases, though:

Perhaps it could be resolved by the type checker or should nested placeholders be forbidden? They don’t seem to be necessary at all.

2 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.