Pre-RFC: make function arguments for Option-typed arguments


#1

This is my first attempt at participating in the language design process. Hope I’m doing it right – meta-feedback welcome!

Motivation

  • Making the simple things easy, but the hard things possible
  • Allowing evolution of APIs with source compatibility
  • Bite off one piece of the optional + default + named arguments improvements
  • Lots of people want it

Proposed solution

fn adder(x: int, b: Option<int>) -> int {
    x + b.unwrap_or(1)
}
fn main() {
    println!("added {}", adder(5));
}

The compiler will fill in None for any omitted function parameters if and only if they are of type Option. This nicely complements impl From<T> for Option<T>.

Drawbacks

  • Default values have to be specified in the function, at the cost of slightly more code there. This seems like an acceptable cost given the limited weight (in terms of ergonomics) of making this change.
  • Makes the None arguments implicit, which makes it slightly harder to see what’s going on.

Alternatives

  • Default arguments (fn adder(x: int, b: int=1) -> int) require a bunch of syntax changes. We would need to impose more restrictions (only allow statics has been thrown around in the past), and the syntax looks kind of ugly in my mind, because there’s not really a natural order from name to type and value.
  • Builder APIs (requires no changes) can be used. However, I’d argue that for many uses this is much more wordy, and requires more work in looking up the proper API calls. Also, I’m guessing it would perform worse at a micro-level, given the amount of function calls.

Unresolved issues

  • There might be some interaction with named arguments in the future. To me, this proposal seems like a clean first improvement that will most likely not make named arguments harder, and hopefully makes it easier, so I thought I’d raise this pre-RFC for optional arguments only.
  • Undoubtedly others can come up with more!

Further reading

I confess to not having read through all of it, but I have skimmed through them and looked at outcomes and resolutions so far. A bunch of these were closed based on an impending 1.0 deadline and wanting to do this in a backwards-compatible ways. In any case, I couldn’t find anything that clearly invalidates the proposal in this pre-RFC.


[Pre-RFC] named arguments
#2

Hi Dirkjan,

Proposed solution

fn adder(x: int, b: Option) -> int { x + b.unwrap_or(1) }

That doesn’t feel that backward compatible, because now suddenly Options at the last position of the argument list get default values, which previously have to be given explicit, and the function author most likely wanted that the user makes a conscious descision.

Look at e.g. the ‘and’ and ‘or’ methods of ‘Option’. Now you would get default values at places where you certainly never wanted them.

So mixing these two concepts doesn’t feel right. Reusing syntax two mean different things IMHO isn’t a good idea.

Alternatives

• Default arguments (fn adder(x: int, b: int=1) -> int) require a bunch of syntax changes. We would need to impose more restrictions (only allow statics has been thrown around in the past), and the syntax looks kind of ugly in my mind, because there’s not really a natural order from name to type and value.

The point about the syntax seems a bit strange, because it’s pretty much exactly the rust syntax for defining variables:

let b: i32 = 1;

Greetings, Daniel


#3

I would rather not make Option<T> known to the language.


#4

Option<T> is already known to the language due to the for syntax desugaring.


#5

I don’t think it really does, unless the behavior is instead implemented for any type parameter bound by Into<Option<T>>.


There’s a larger question in this RFC (and some others, like delegation) about how Rust should be designed. Currently, there is very little ergonomics language magic (for and ? are the big exceptions). This has a certain elegance and can be more comprehensible - reading the recent blog post about how for works, I was struck by how many moving parts it has that I have just internalized. However, ergonomics language magic is, y’know, ergonomic, and I think that can result in algorithms that are easier for new users to understand, even if the language is harder to understand. I feel very torn on this question.


#6

I feel uncomfortable with this proposal.

I think this proposal has a lot of overlap with default parameters and could become an obstacle if we ever decide to move forward with default parameters. Imagine this hypothetical function signature:

pub fn foo(a: Option<i32> = Some(42)) {}

Now what rule gets precedence? I guess it would be the default parameter, but it would get very confusing, even more so as Option<T> would be the only type to have a special case.

Another thing that bothers me is that API authors sometimes prefer to require an explicit None instead of leaving it blank. I take an example from a thread the OP linked to.

fn timeout(&mut self, dur: Option<Duration>) {

}

thing.timeout(); // this would not make sense
thing.timeout(None); // you must give an answer
thing.timeout(Some(10));

Sometimes it makes sense to require an explicit None and it feels wrong to take away this control from API authors to give it to the consumer. Here again I think default parameters would be the better choice because it would allow the same kind of ergonomic APIs but still allow API authors to state their intent clearly.

In my opinion, default parameters are the more general form of this proposal.


#7

Thank you for all the feedback. It has caused me to do a lot more thinking, which I’ll now try to reflect here.

@dan_t you’re right that the syntax for defining a local variable is very similar, so the alternative of real default values should stop complaining about the syntax for it. On the other hand, thinking about it more, teaching the limitations of what can be put in a default value should be a strong argument against that direction in my opinion.

Also, you make a good point about the API author not being able to require an explicit choice from the API user. Let’s state this in a drawback:

Drawback: the API author can not easily require the author to explicitly select a value for an Option-typed parameter.

Alternative: we could introduce a new symbol separating arguments in the function signature, to tell the compiler that only arguments after that symbol are really optional. This is informed by Python’s * argument separator that is used to introduce keyword-only arguments. Instead of the *, perhaps ? (surrounded by commas) can be used for this. I’m not sure this adds enough value to be worth the cost in function signatures with extra symbols that will require some teaching due to lack of familiarity.

Alternative: we could introduce a new, Option-like enum, like enum Default<T> { Value(T), Missing } which allows the API author to explicitly enable optionality of the argument. However, in naming this type, I would actually feel that Optional is a better name, which makes me question its value compared to Option.

@withoutboats interesting point about Into<Option<T>>. I guess that should result in an added alternative:

Alternative: allow the compiler replacing missing values with None for anything that implements Into<Option<T>>. My current feeling is that (alluding to your other point) doing so would make this proposal more magic, without a corresponding increase in ergonomics.

@Azerupi you’re right that putting this together with default values would result with semantics where there’s no “natural”, DWIM choice between two alternatives. From this, I came with up with more drawbacks and another alternative.

Drawback: this proposal makes a future default value proposal harder or impossible, since there is no natural resolution for a default value if the argument has an Option type. I would indeed argue that the remaining value of default values if this proposal is accepted is very limited, and so this proposal should be good enough so that default values will no longer be desired.

Drawback: for arguments that have a domain which includes a sentinel value where None is currently used (i.e. timeouts), this makes it impossible to have a default other than None. Here, I would argue that the alternative solution for this that comes to mind would be to introduce enum Timeout { Value(Duration), NoTimeout }, which I think is actually better in being more explicit about the meaning of the sentinel value (contrast None as not-a-value to None as sentinel-value). However, Option<Duration> is currently used in std for timeouts: 6 locations found in sys and net (though some of these are platform-dependent, and so they are probably not all independent). Note that this proposal, by itself, still does not create backwards incompatibility concerns (since existing code will keep working).

I guess I should make it more explicit that the underpinning of this proposal is that “not passing a value” feels pretty close semantically to “passing a None value”. However, perhaps I’ve understated this difference. Let’s add one more drawback and alternative.

Drawback: it might not be clear in the caller that some arguments are being added. While it is explicit, in the sense that you can clearly see how many arguments are being passed in, this might not be as obvious in functions with a larger number of arguments. It’s also not explicit-explicit, in the sense that there’s a marker to indicate that the compiler will extend the call with more arguments.

Alternative: add a marker token, like ..., to indicate in the caller that the compiler should fill in further arguments. However, this then makes the API evolution goal impossible, at least for the case of going from zero to one or more optional arguments.

Finally, I also wanted to state that I have a lot of experience with Python, which allows default values for arguments as well as named parameters (and more recently, keyword-only arguments). This proposal is informed by my experience that in Python, a large majority of all default values seem to be None. If useful/necessary, we could try to query all Python code in the GitHub data set to validate this assumption.


#8

So, discussion’s died down. Ideally, I think I should get some acknowledgement from the language team or something, before moving on to write a full RFC?


#9

Sorry, I missed this thread somehow. Personally I’m not really a fan of automatically converting any Option<T> argument into something optional: it’s not clear that this was the intention of existing code, for one thing. I agree it’s technically backwards compatible, but it doesn’t seem to necessarily preserve the spirit of the existing code.

I also don’t like that including the value for a default argument means I have to write Some, nor that all default arguments must have option type – frequently I want to use have an integer argument that defaults to 0, for example, or some other constant.


#10

What about declaring optional arguments like this:


fn takes_optional_arg(x: i32, y: i32 = 0) {
  ...
}

and call it as

takes_optional_arg(1);
// equivalent to
takes_optional_arg(1, 0);

// override the default
takes_optional_arg(1, 2);

#11

@nikomatsakis thanks for the feedback. I only added the “language design” label recently, does that influence how you read the forums?

Also, I listed a number of alternatives in both the initial pre-RFC and follow-up posts. To recap:

  • Default arguments (fn adder(x: int, b: int=1) -> int) require a bunch of syntax changes. We would need to impose more restrictions (only allow statics has been thrown around in the past).

  • Builder APIs (requires no changes) can be used. However, I’d argue that for many uses this is much more wordy, and requires more work in looking up the proper API calls. Also, I’m guessing it would perform worse at a micro-level, given the amount of function calls.

  • We could introduce a new symbol separating arguments in the function signature, to tell the compiler that only arguments after that symbol are really optional. This is informed by Python’s * argument separator that is used to introduce keyword-only arguments. Instead of the *, perhaps ? (surrounded by commas) can be used for this. I’m not sure this adds enough value to be worth the cost in function signatures with extra symbols that will require some teaching due to lack of familiarity.

  • We could introduce a new, Option-like enum, like enum Default<T> { Value(T), Missing } which allows the API author to explicitly enable optionality of the argument. However, in naming this type, I would actually feel that Optional is a better name, which makes me question its value compared to Option.

  • Add a marker token, like ..., to indicate in the caller that the compiler should fill in further arguments. However, this then makes the API evolution goal impossible, at least for the case of going from zero to one or more optional arguments.

Can you indicate which one of these you find most promising (and why)? (Or explain why you care for none of them, I guess!)