Skip to content
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

optional parameters #152

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
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
116 changes: 116 additions & 0 deletions active/0000-arity-parameter-overloading.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
- Start Date: 2014-07-03
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)

# Summary

This RFC proposes to add arity-based parameter overloading to Rust.

# Motivation

Currently in Rust there are a lot of functions that do the same thing, but take a different number of parameters.
The current design forces those functions to have different names.
This means that sometimes it's harder to look up function names because they are completely unrelated.

# Detailed design

Java has a very complicated overloading design that includes overloading by static types.
Overloading on types mixed with type inference might be very confusing.
However, overloading based on arity is very simple and clear.
Nobody will be confused by which method is being called when they differ by how many arguments they have.

```rust
fn concat(&self) -> String {
...
}

fn concat(&self, sep: &str) -> String {
...
}

// compile error, because the first parameter's type
// doesn't match the type of `concat` already declared.
Copy link
Member

Choose a reason for hiding this comment

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

We don't currently have any kind of ordering for declarations, right? This introduces some kind of ordering.

fn concat(&mut self, sep: &str) -> String {
...
}

// compile error, because the second parameter's type
// doesn't match the type of `concat` already declared.
fn concat(&self, number: int) -> String {
...
}
```

So `to_str_radix(&self, radix: uint) -> String` can be now written as `to_str(&self, radix: uint) -> String` while
`to_str(&self) -> String` still exists. This will let Rust get rid of the sheer multitude of functions that only
differ by a few parameters like `split` and `splitn`.

Default arguments almost solve this problem, but they don't solve the problem with `unwrap` and `unwrap_or`.
Arity-based overloading allows you to have `unwrap(self, default: T) -> T` as well as `unwrap(self) -> T`.

This also allows you to return a different type. Again, these are different functions with the same name.

```rust
fn split<Sep: CharEq>(&self, sep: Sep) -> CharSplits<'a, Sep>
fn split<Sep: CharEq>(&self, sep: Sep, count: uint) -> CharSplitsN<'a, Sep>
```


# Drawbacks

Compared to default arguments, it is much more verbose and gives more power to the user.

1) Lets you return a different type
2) Lets you omit arguments completely for a different implementation

However, as you can see from the examples, `to_str`/`to_str_radix` only requires default arguments to be combined into one function.
`split`/`splitn` and `unwrap`/`unwrap_or` require overloading to be combined. One could make an argument that default arguments have
a better syntax and are less verbose. Implementing default arguments instead might be better.

# Alternatives

The aforementioned default arguments are a strong alternative since it's a lighter syntax.

Current APIs have three slice functions:

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

This proposal does not let you have

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

This is because Rust does not support keyword arguments. You can't distinguish between a beginning and an end.
If Rust did support keyword arguments you could call those functions like this:

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

Overloading on keywords a la Smalltalk is the most powerful and allows the most freedom in API design.
However, it should be left to another RFC since it is a separate idea from default arguments and overloading altogether.

# Unresolved questions

Would it be beneficial to implement both overloading and default arguments?
In a lot of cases, you want just default arguments, like in the case of `to_str` where you just want to write
`fn to_str(&self, radix = 10u) -> String` or `fn to_str(&self, radix: uint = 10u) -> String` without type inference.

Having to write two type signatures like

```rust
fn to_str(&self, radix: uint) -> String
fn to_str(&self) -> String
```

seems like it is too verbose even if overloading is strictly more powerful.
104 changes: 104 additions & 0 deletions active/0000-optional-parameters.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
- Start Date: 2014-07-03
- RFC PR #: (leave this empty)
- Rust Issue #: (leave this empty)

# Summary

This RFC proposes to add optional parameters to Rust functions.

# Motivation

Currently in Rust there are a lot of functions that do the same thing, but take a different number of parameters.
The current design forces those functions to have different names.
This causes the standard library to have badly-named functions.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't argue that some of our stdlib functions could be better named, but finding better names is not impossible. I also don't argue that optional parameters will allow strictly nicer names some of the time; god knows I've wished for them before. But keep in mind that up till now our stance has been that being forced to name your functions explicitly has been considered a feature, and that the lack of overloading/optional parameters has been an explicit choice (though entirely subjective). To overcome this inertia, you'd be best served by showing before/after comparisons of how real-life stdlib functions would look (both in definition and in usage) if this change were accepted.

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 I'd rather resubmit this as Java-style overloading. It's easier with static typing + no nulls. In C# you'd just go with unwrap(default = null) and call it a day. Not so easy in Rust since Option is more syntactically noisy.


# Detailed design

Optional arguments are an implicit form of overloading.
Java has a very complicated overloading design that includes overloading by static types.
Overloading on types mixed with type inference might be very confusing.
However, overloading based on arity is very simple and clear.
Java also forces the writer of the function to write out a new function signature for every possible variant.
A nice syntax for optional parameters is preferable to the Java approach.

So `to_str_radix(&self, radix: uint) -> String` can be now written as `to_str(&self, ?(radix: uint))`
Since this is sugar for full overloading, a function declared with an optional parameter like this satisfies both
```rust
pub trait ToStr {
fn to_str_(&self) -> String;
Copy link
Member

Choose a reason for hiding this comment

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

Is the trailing _ a typo?

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, it's a typo

}
```

and

```rust
pub trait ToStrRadix {
fn to_str(&self, radix: uint) -> String;
}
```

traits.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand what you mean by "satisfies both traits". Rust isn't duck-typed, and therefore you only satisfy a trait if you explicitly implement it. Merely implementing a function with a compatible type signature doesn't allow you to do anything generically.


Inside the body of the function, the arguments will be pattern matched something like this:

```rust
let rad = match args {
Copy link
Member

Choose a reason for hiding this comment

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

What is args? An implicit variable in every function? What is its type?

Copy link
Member

Choose a reason for hiding this comment

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

Also, this implies that functionality that was previously "executed" at compile time (i.e. calling a different method to get different behaviour) would now, instead, be executed at runtime, as a switch over the number of arguments. Am I interpreting this correctly?

Copy link
Contributor

Choose a reason for hiding this comment

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

Is this something like Javascript's implicit arguments variable in each function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's a problem, since the contents have different types.

[_, radix] => radix,
_ => 10
};
```

This allows for default arguments or actually doing completely different in each case.

This will let Rust get rid of the sheer multitude of functions that only differ by a few parameters like
`concat` and `connect`; `split` and `splitn`. You can even go further and have a boolean indicating whether to
use terminator semantics and put that into `split`, eliminating `split_terminator`.
You could also have another boolean indicating whether it is reversed, eliminating `rsplitn`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a note here, but boolean flags are an API anti-pattern. I'd rather have separately-named functions.


In this way, the library design is better, allowing auto-completion on `split`.
Copy link
Contributor

Choose a reason for hiding this comment

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

In what way does the lack of optional parameters impede autocompletion?

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 could want to "concat by character" and be looking for all functions that start with concat but it's actually connect.

There are less names in the std library. Some naming problems go away.

The easiest design is to allow all trailing parameters to be optional.
That means that no mandatory parameter can come after an optional one.
This design still allows to simplify naming in the standard library before 1.0 ships and the names are set in stone.
Further refinements like trailing varargs, keyword arguments are possible in a backwards-compatible way.

# Drawbacks

Currently, you are able to write a function that differs by name to achieve the same effect.
This works for `slice_from` and `slice_to` and `slice`.
They are aptly named for what they do and can be easily autocompleted.
However, just one `slice` with two optional args (defaulting to 0 and the length of the string)
Copy link
Member

Choose a reason for hiding this comment

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

Could we have code examples for how the new slice function would be declared and called? (Having more examples would be good.)

Copy link
Member

Choose a reason for hiding this comment

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

Sorry, I'll strengthen that: this needs more examples of declaration and of calling (e.g. I don't see any use of the ?(...) syntax other than the isolated mention in the text): this proposing a syntactic/ergonomic change so we need to be able to see what it looks like with clarity.

is much more elegant and doesn't clutter up the standard library with extra functions.

Of course, as I mentioned earlier, this also interacts with traits since now you satisfy two traits with one function.
Copy link
Contributor

Choose a reason for hiding this comment

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

Another reminder that this is not how traits work in Rust (see above).

This probably interacts with closures and/or lifetimes in some way as well.
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, it would be impossible for any function to return a reference that in any way relied upon the lifetime of an optional input.

So the correct standard library design must be weighed against adding yet another feature to Rust.

# Alternatives

Another proposal is to somehow represent optional arguments as some kind of an Option type.
The drawback of this proposal is that Option is a library type. It would have to be baked into the language instead.

Another alternative is to keep the full overloading syntax. This eliminates having to destructure the args array.
While this makes it a pain to rewrite all the possible variants, it's extremely explicit and clear.
If you want either one or three arguments only, it won't accept two.
For any function with k mandatory parameters and n total parameters the current proposal accepts all arities between k and n.

An alternative to trailing optional arguments is keyword arguments. This allows optional arguments in any place as long as
the following required arguments or following optional arguments are called by their keywords to resolve ambiguities.
This proposal has the downside of a more complicated argument resolution system (allowing some arguments to be called by position
and some by keyword). It can also be implemented in a backwards-compatible way post 1.0 so it's not a 1.0 priority.

# Unresolved questions

If destructuring the arguments array is necessary, should there be some kind of a keyword for them.
Is there any way to implement it without using a keyword, other than Java-style overloading?

If varargs are included in the proposal, then the `main` function could be written as

```rust
fn main(...arguments: String)
```

which could be nice. But then again, using std::os::args() is not a huge problem.