An approach to resolving some issues with optional arguments

Some issues in implementing optional arguments have been:

Implicitly calling expensive new functions to construct the defaults

Calling the right drop method 

My proposal is to restrict the default values for optional parameters to be Copy. This would make it cheap to construct defaults and trivial to drop. This approach would rule out having a Vec or Option<Vec<_>> as an optional argument. However, allowing default arguments for copy types would still be very useful. Types for Enum Variants, if implemented, would allow None to be a default argument for any option type.

Given that once you start using defaults for params the order starts to get even more problematic, I think it would be must better to have some shorthand for "fill in the rest of the fields of this struct with Default::default and compile error if a field's type doesn't implent it".

That would be much more useful in general. Outside of just functions.

Edit:

To go even further such a feature could interact with non_exhaustive in such a way as to have a semver-minor way of having a structure with some required fields and the rest optional without having a builder.

2 Likes

The approach which makes the most sense (to me) is just to have each default be a const item. The default is const evaluated, and then calling the function without a default argument is just sugar for foo(arg, DEFAULT_ARG). There's no question about what the semantics of such a feature would be.

The actual blocker is more on other interesting questions/limitations:

  • Can you use fn foo(u32, u32=0) as fn(u32)? As impl Fn(u32)?
  • Can you default non-tail arguments? How much of std is going to feel second-class because its arguments were put in the wrong order for defaulting?
  • Everywhere else fields are elided, we use .. to indicate that things are missing, why is function defaults different?
  • How do argument defaults interact with traits?
  • What about defaulting arguments of generic type?

There's a number of unanswered questions, along with whether we actually want default arguments, or if we'd be better served by supporting the arguments struct pattern more directly. Default arguments have a tendency to lead to functions with a ton of defaulted arguments, and putting them in a struct makes for cleaner expansion and named (field) arguments.

In a future Rust, I envision something like

// pretend this is an impl
fn Database::new(struct {
    port: Ip,
    file: File,
    trace_span = None::<Span>,
    config = Config::default(),
    ..
});

If we had something like this, there's much less need for default arguments.

8 Likes

And having them in a struct also makes is way easier to wrap that function, to share a set of them between different calls, expand the list later compatibly, etc.

It's even a good idea in languages that do have default & named arguments, like C#. For example, in C# things often take one options argument that's a type with lots of things (like ItemRequestOptions Class (Microsoft.Azure.Cosmos) - Azure for .NET Developers | Microsoft Docs).

4 Likes