Skip to content

Keyword arguments #805

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

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
139 changes: 139 additions & 0 deletions active/0000-keyword-arguments.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
- Start Date: 2015-01-26
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)


# Summary

Add keyword arguments to Rust in a backwards-compatible way.
This allows overloading that is safe in regards to type inference while being decided at compile time.

# Motivation

Allow another kind of argument in Rust. Current arguments distinguish themselves from other types by their order.
If there is no semantic reason for a certain order, the argument order in functions is basically arbitrary.
Consider a call like this:
```rust
window.addNewControl("Title", 20, 50, 100, 50, true);
```

First of all, the call tells nothing the user about what those values mean.
Secondly, their order is arbitrary and must be memorized. What I propose is that this call would rather look like:
```rust
window.addNewControl(
title => "Title",
xPosition => 20,
yPosition => 50,
width => 100,
height => 50,
drawingNow => true);
```

While you might argue that this is more verbose, this is already the standard in the JavaScript ecosystem.
A lot of libraries in JavaScript actually have a convention with calling with associative arrays like:
```JavaScript
window.addNewControl({ xPosition: 20, yPosition: 50, width: 100, height: 5,
drawingNow: true });
```
If this wasn't a convenient pattern, nobody would bother to do it.

Choose a reason for hiding this comment

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

If this is good enough for JavaScript, why is it not good enough for Rust? What about reformulating it as fn add(&mut self, Control) where Control is a structure with the given fields. That would also allow for users to omit fields as "optional" arguments using the ..Default::default() syntax; obviously a convenient default wrapper function would help there, but the point still stands.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because you have to declare a struct manually, while in JavaScript everything is ducktyped. It is considerably more difficult to do the same thing in Rust. Also, by allowing keywords you get overloading. You would have to make some kind of Traits to achieve the same thing in Rust.

Copy link

Choose a reason for hiding this comment

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

Overloading in Rust is IMHO absolutely unneded. We should use meaningful function names instead of that. Keyword arguments are different way of using object factory and IMHO there is no need for them as we can easily simulate them by using struct like @DanielKeep said. Only (and controversial) income from keyword arguments would be that we can reorder arguments in any way, i.e.

fn from_cartesian(x: i64, y: i64) -> Self {}

Point::from_cartesian(y => 2, x => 4)

And that will save us from checking documentation for arguments order. I would oppose this if it will be tool to add function overloading as I consider it harmful.

Choose a reason for hiding this comment

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

I'm bringing this up because it's a fairly obvious hole in your argument that you simply don't address. I mean, you could take the argument object pattern in JS as an indication that keyword arguments are desirable or that just passing an aggregate structure is a perfectly viable alternative. You should address both sides honestly and fairly, which will strengthen the RFC.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I would argue that the function's keywords are part of its name so you'd have a function from_cartesian(x=>,y=>) that would correspond to a Smalltalk message send like fromCartesianWithY: x: except you still have a function name so you don't have to smoosh that into the first argument. So you can't "overload" with another from_cartesian(x=>, y=>) that accepts f64 or any other kind of silly overloading other than the function name and its keyword arguments.

Copy link
Member

Choose a reason for hiding this comment

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

If this is good enough for JavaScript, why is it not good enough for Rust?

I don't have any position on this RFC yet, but there are many things that are good enough for other languages that aren't for Rust. Garbage Colletion being a primary example.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note that because Rust requires you to name the type of the struct in a struct literal, the syntactic overhead is much higher than in JS.


An additional benefit is that this leads to an easy implementation of optional parameters and optional parameters.
In languages like PHP, default parameters must come at the end of the function and must have a value.

You can have optional parameters in the beginning, middle and end of functions without any default value.
This means that this design allows for truly optional parameters without any run-time cost of using an `Option`.

# Detailed design
Currently Rust has

```rust
fn slice(&self, begin: usize, end: usize) -> &'a str
fn slice_from(&self, begin: usize) -> &'a str
fn slice_to(&self, end: usize) -> &'a str
```

This can be changed to

```rust
fn slice(&self, from => begin: usize, to => end: usize) -> &'a str
fn slice(&self, from => begin: usize) -> &'a str
fn slice(&self, to => end: usize) -> &'a str
Copy link

Choose a reason for hiding this comment

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

Nope. I would vote against this. Why introduce another keyword when we have one? We could use to => usize instead as signature that this is keyword argument or default arguments, so this will be:

fn slice(&self, begin: usize = 0, end: usize = /* something, maybe `self.length()` */) -> &'a str

Anyway syntax need reconsideration.

Choose a reason for hiding this comment

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

@hauleth: I think it's a little disingenuous to pick on the specific example. I can go one better: why have a slice method at all when we now have overloadable indexing and native range syntax? I agree a better example could be chosen, but that's not really the point of this.

Copy link

Choose a reason for hiding this comment

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

@DanielKeep I could use any example (but this one was poor anyway). Although the meritum was here. Why introduce new keyword when we have one already? We should reuse what we have instead adding some creepy syntax (like Ruby have done).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Default/optional arguments are compatible with this design. They're really sugar for a form of overloading, so they can be added later if keyword arguments are added. Or maybe people will simulate them with macros and it won't be necessary.

The point is that this is outside of the scope of just adding keywords. You can argue merits and syntax for default arguments, but that's not the same concern as the keyword arguments themselves.

```

Note that these are three different functions that have three different signatures.
The keywords a function accepts is part of its signature. You can call these functions like this:

```rust
foo.slice(from => 5); //equivalent to current foo.slice_from(5)
foo.slice(to => 9); //equivalent to current foo.slice_to(9)
foo.slice(from => 5, to => 9); //equivalent to current foo.slice(5, 9)
foo.slice(from => 9, to => 5); //equivalent to current foo.slice(5, 9)
```

The trait looks like

```rust
Fn(&self, from => begin: usize, to => end: usize) -> &'a str
//which desugars to
Fn<({from: usize, to: usize}, &Self), &'a str>
```

Choose a reason for hiding this comment

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

What about function types? The slice method would have a subtype of for<'a> fn(&'a str, usize, usize) -> &'a str which is not the same as the Fn trait.

Choose a reason for hiding this comment

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

What is the desugaring if there are no keyword arguments?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same as now? I thought this could be added to affect only the functions for which keyword arguments actually exist

Choose a reason for hiding this comment

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

But then, how do you tell the difference between a function that has keyword arguments and a function that has a struct as its first/last argument?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I imagine that this would be equivalent, and you could call a function that does this now with the keyword syntax in the future if this is implemented. Although I'm not sure if my vision for keywords being a part of the function's signature is compatible with that.

Choose a reason for hiding this comment

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

These are the sort of details I think need to be addressed in an RFC. You've said that:

fn slice(&self, from => begin: usize, to => end: usize) -> &str

Desugars to

for<'a> Fn<({from: usize, to: usize}, &'a str), &'a str>

But what about

fn as_slice(&self) -> &str

What does that desugar to?

for<'a> Fn<(&'a str,), &'a str>
for<'a> Fn<((), &'a str), &'a str>
for<'a> Fn<({}, &'a str), &'a str>

Also, given that the Fn* traits aren't even fundamental types, I think you have to define what happens to fn types. And how that interacts with FFI.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I just addressed it here. fn as_slice(&self) -> &str desugars to the same thing it always desugared to, for<'a> Fn<(&'a str,), &'a str>, nothing has changed because it doesn't have keyword arguments so this proposal doesn't affect current code.


So this is equivalent for using a `struct` to pass around your keyword parameters.
But now you are able to use the same function name several times as long as the keywords are different.

# Drawbacks

This is a more complicated design than just having default arguments and overloading.
Now there are both positional and keyword arguments and they interact with lifetimes, traits, and closures.

# Alternatives

A better design to the above function might be designing it like so:

```rust
let title = "Title";
let position = Position(20, 50);
let dimensions = Dimension(100, 50);
window.addNewControlDrawingNow(title, position, dimensions);
```

Choose a reason for hiding this comment

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

This isn't really an alternative; both because it's something you can do right now and because you have to remember the order. I think this falls under the traditional "alternative" of "don't change anything".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, that's the "do nothing" alternative.


Now the function takes three parameters, and we assigned them meaningful names.
Instead of passing a boolean, we created functions that do different things.
They could be named something like `addNewControl` and `addNewControlDrawingNow`.

While this design is better, it still doesn't solve the problem of having to remember of the order.
Where do you put `dimensions`, `position`, and the `title`?
At least the compiler will now verify that the types are correct.
It is still up to the programmer to actually name those variables well, instead of the API specifying what the keywords should be.

If keyword arguments themselves are not implemented, then there's also the issue of overloading to enable better API design.
Another proposal would be to assign keywords to all current Rust code with their local names.

Choose a reason for hiding this comment

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

This really threw me for a loop: "assign keywords". This implies that you want to make all existing argument identifiers reserved keywords in the language or something. Unfortunate overloading of the term, I suppose.

Maybe reword to something like: "allow any argument to optionally be used as a keyword argument".

So something like:

```rust
foo.slice(begin => 5, end => 10);
```

This will solve the problem of the naming of parameters, but won't solve the problem of overloading.
Because this design this allows you to call this method like:

```rust
foo.slice(x, y);
```

This means that you potentially can't infer the types of the arguments if they were overloaded.
Another overload could add another `slice()` that takes a different type of parameter in the first slot.
What type would `x` be then?
Even though it has a different name, being able to call keyword arguments without keywords breaks overloading.

With mandatory keywords it is always known what type a certain keyword is.

Choose a reason for hiding this comment

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

You haven't included the alternative that you demonstrated being used by JavaScript: passing a struct or enum.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is not an "alternative" since my proposal is sugar for this very thing. What you're really passing is a struct of arguments that have keywords, but you don't have to actually declare it.

Choose a reason for hiding this comment

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

It's something you could do instead to achieve the same effect. An RFC is about making a solid technical and practical argument for why the change should be made. If you ignore practical, viable alternatives, it just gives people ammunition to blow holes in your argument with. :)

It would be better for the RFC to acknowledge the alternative, then address why you feel it's not sufficient.

In this particular case, yes, the sugar is going to be shorter, but I don't see it as being a huge imposition to do it by hand. The RFC should convince me otherwise, show me the objective light!

Copy link

Choose a reason for hiding this comment

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

What about using the same structure syntax as javascript as sugar ? Something like :

fn foo<T>({ self, len : usize, f : Fn(T) -> u32 }) -> u32

I don't see why it would be an issue. Indeed, structure literals require a struct name, and in this case you would just call the function like this, without struct name:

x.foo({f : |x| x +1, len  : 42us }).

Alternatively, if you want only reordering, and no overloading, you can still call the function with the default order :

x.foo(42, |x| x + 1)

I feel like this is a viable alternative, or am i missing something ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think self should be a positional argument. The problem I have with this design is that you may put the struct containing keyword arguments in any position.

fn foo<T>(self, {len : usize, f : Fn(T) -> u32 }) -> u32 or is it
fn foo<T>({len : usize, f : Fn(T) -> u32 }, self) -> u32?

I would like some extra sugar so that there is only ONE struct with keyword arguments and it's in a predictable location.

# Unresolved questions

The trait desugaring is an addition based on the discussion on the internals board.
Could it come first (before the self parameter) or does it have to come last?

Another possibility is a different syntax for keywords. Maybe it would be better to unify it with the struct syntax.
So something like `foo.slice(begin: 5, end: 10)` because it desugars to a keyword struct anyway.

Choose a reason for hiding this comment

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

Unlikely to be possible while leaving the door open for type ascription.

Copy link

Choose a reason for hiding this comment

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

To repeat a claim I've made on discourse: syntactic consistency between two very similar operations - this and struct construction - is more important than type ascription.


But what would the declaration look like?