Optional Fields
- Feature Name: Optional Fields
- Start Date: (fill me in with today's date, YYYY-MM-DD)
- RFC PR: rust-lang/rfcs#0000
- Rust Issue: rust-lang/rust#0000
Summary
Adds an optional ?
syntax after a struct's field's type, which is equivalent to wrapping the type in an Option
. Additionally, when using struct literals of optional types, these fields may be omitted and automatically filled in as none. If they are specified, they do not need to be wrapped in a Some
.
Motivation
A common pattern is having a large struct with many optional fields, which are often used for configuration. This is then passed to a function which matches against them, and acts accordingly. Currently, there are two options:
Using a builder
Builders are a very common way to generate a struct with many optional fields. The builder is created via Default
, or another constructor method, then every field has its own associated function which configures the struct. There are, however, a few drawbacks:
-
For every field, you need to create a corresponding method. This can get unwieldy when using a large number of them. Of course, macros can help here, but they have limits and also can get messy.
-
Also, each field has to have
Option
wrapped around the type, which can get annoying and in the way too. -
The associated functions can either take a reference (
&mut self
), or an ownedself
(and return the same). References are great because they not only allow one-liner configuration, but also allow easy optional configuration (foo.bar(3);
). However, if the object needs to be owned to configure upon, one-liner configuration is impossible (you need to store the builder in a variable).For an owned
self
, the opposite problem occurs: one-liner configuration is still possible, but at the cost of annoying optional configuration (foo = foo.bar(3);
).
Using a struct literal
The other option is to not use a builder. The struct is created in largely the same fashion, but without the hassle of implementing a method for each field. Then, the struct is built using a literal, often using struct update syntax, like:
{
foo: Some(3),
bar: Some(5),
..Default::default()
}
This:
- Means you need to wrap every value in
Some
, which can get messy when configuring many fields - Means you need to write
..Default::default()
at the end of a configuration. For many nested configuration structs, this gets out of hand pretty quickly - The problem of wrapping every type in
Option
still persists.
Why specifically Option
? Why does this merit a special case?
Option
is used far more than other types when it comes to long structs due to cases like configuration structs as said before. You get massive builders that consist of many many Option
s. Thus, if they can be specified in a struct literal without having to add builder methods (which have other downsides as explained in this RFC). And if it didn't automatically wrap the value in Some
, builder methods would have that rather large advantage; nobody wants to write 20 Somes for configuration.
Guide-level explanation
When defining a struct, you can add an "option modifier", represented by a question mark (?
) modifier after a field type. This will convert the type T
to be Option<T>
. For example, say we had a user struct:
pub struct User<'a> {
pub user_name: &'a str,
pub full_name: &'a str?,
pub description: &'a str?
}
is equivalent to
pub struct User<'a> {
pub user_name: &'a str,
pub full_name: Option<&'a str>,
pub description: Option<&'a str>
}
This is useful in structs with many Option
s, like configuration structs.
Additionally, when you are using a struct literal with an option modifier field, you may omit it (note this doesn't apply to normal types specified with Option
). If you do specify it, then you don't wrap it in Option
; you only specify the inner type. For example, you could create the above User
with
User {
user_name: "Ferris",
full_name: "Ferris The Crab",
description: "Rust's unofficial mascot"
}
or
User {
user_name: "scoobydoo",
full_name: "Scooby Doo"
}
In some cases you might need to use an option here (specifying full_name: Some("Scooby Doo")
is invalid). In this case, you can use the special syntax of an option modifier after the field name, i.e
fn get_full_name() -> Option<&str>
User {
user_name: "abc123",
full_name?: get_full_name()
}
An example to clarify how this would work, using generics:
struct Foo<T> {
bar: T?
}
Foo {
bar: None // implies that `T` must be `Option`. The field `Bar` is Option<Option>
}
Foo {
bar?: None // does not imply info about `T`, since it's `None`.
}
Note, this syntax isn't supported at all for tuple structs. These aren't complex enough to merit this behaviour (complex tuple structs should be converted to structs with named fields), and can have ambiguity (How do you make a tuple struct (u32, u32?, u32?)
with the second left out?)
Reference-level explanation
Option modifier
The new syntax for struct fields would be
StructField:
OuterAttribute*
Visibility?
IDENTIFIER ":" Type
OptionModifier?
OptionModifier: "?"
Fields with option modifiers would be equivalent to an Option<Type>
in all places except struct literals. This means, for the User
struct above, to change a field, you would do
let mut user = User {...}
user.full_name = Some("John Smith");
Accessing fields, setting them, referring to them etc. all use the Option
type. Struct literals are just a special case.
Rustdoc would also have to change to show these struct definitions correctly.
Struct literal syntax
The new syntax for struct literal fields would be
StructExprField:
IDENTIFIER OptionModifier?
| (IDENTIFIER OptionModifier? | TUPLE_INDEX) ":" Expression
As seen in the syntax above, we can do something like
fn get_full_name() -> Option<&str>
let full_name = get_full_name();
User {
user_name: "abc123",
full_name?
}
The option modifier after a field's name, for an optional fields, prevents the auto-Some
behaviour. The reason it goes here is because if it were to go after the value, it would be ambiguous with the try operator.
If a struct is composed purely out of optional fields, then the unit syntax (i.e the struct name without curly braces) can't be used, despite no fields being set. This is because it may signal intent differently; the unit syntax implies a zero-sized-type. The type would be completely null but would still take up space.
The reason using an option without the option modifier flag (e.g User {full_name: Some(""), ...}
) is invalid syntax is because it could be ambiguous with generics (the compiler would struggle to infer full_name
, if it were a generic type T
).
If an option modifier is supplied after a field name that isn't an optional field, then it should result in an error, since the user probably meant for something else.
Drawbacks
- Fields have to be tracked to check if they have an option modifier
- The struct syntax is slightly more complex
- It could be tougher for beginners to understand, though this is not necessary to explain until later on in the book
Rationale and alternatives
- This design is simple, not breaking, and solves the problems well
- Not doing this would mean configuration structs, especially nested ones, remain annoying to define and instantiate
- The two different positions for this operator show different intents, as they represent different things.
Reasoning footprint
The rust blog has talked about the reasoning footprint, and how implicit features (this being one of them) should act. There are 3 categories:
-
Applicability: The implicit wrapping of
Option
is explicit, since the option modifier is used. In a struct literal, this is less explicit, as it looks like a regular field declaration, but there is some heads-up since the field is marked explicitly in the definition. (And the prevention of this auto conversion, with the option modifier after the field name in a struct literal, is explicit) - Power: This is quite powerful, changing a field's type and a value's type. This is probably the largest category, limiting the other two.
- Context-dependence: In the struct declration, no context is required at all. In the struct literal, some context is required to see if the field is optional, though this context is already available and used anyway to determine the available fields and types.
Why specifically structs?
This idea could definitely be applied to other contexts, but it would have to be done carefully. Structs are something easy to update now, and would benefit most from this.
- Function parameters having this syntax would allow optional arguments of sorts, and this would be great, though not everyone likes this
- Function return types could also benefit from this, though it'd probably need some adaptation for
Result
s too.
Alternatives
-
A small note: you can use
.into()
instead of wrapping a value inSome
. Not really an alternative but it looks slightly nicer, but is still verbose and doesn't save characters. -
This RFC pre-draft for default values would help with the option modifier in struct definitions: essentially you could write
= None
instead of?
as proposed. However, in a large struct with manyOption
s, this is quite long and annoying, taking up a lot more space than?
(though I support that RFC, other default values would be nice).Another problem with only setting defaults is the need to still wrap the values in
Some
, which doesn't solve the problem on the user's side. This RFC also proposes a..
syntax which is equivalent to..Default::default()
, which would help partly but still be annoying in deeply nested structs. -
This RFC pre-draft suggests using
..*
as syntatic sugar for..Default::default()
, similar to above. Again, it's annoying within nested structs and while it shows intent a bit clearer for more default values, for optional fields it doesn't really help. -
Add more support for
Option
s with syntax in general. Combined with a shortened..default()
this could work well
Prior art
This sort of syntax is common in many languages, such as typescript, or kotlin. Typescript has the same syntax for declaring them, and their omission/inclusion in object literals represents their presence or null. Of course, rust's Option
needs the optional option modifier in the struct literal syntax, whereas in typescript, a value is effectively Some
already and undefined
is None. undefined
represents something that hasn't been initialised, which corresponds well to rust's Option
.
It's so integrated into typescript that there are even standard library types (that you can implement yourself, using type mapping) that, say, convert all properties in a class to be optional. This is not a good idea for rust since it can greatly complexify types, and type mapping would be quite hard to implement, but that's not for this RFC to discuss.
Languages like typescript and kotlin also have syntax very similar to rust's for handling null objects: they have the null coalescing operator ?.
which is equivalent to Option::map
. In rust, using the same syntax invokes the try operator, which has similar intentions, but returns from the whole function if the value is null.
Unresolved questions
- Is the option modifier on struct literals indeed the best design?
- How exactly should rustdoc show this?
- Should the error for specifying an option modifier in a struct literal on a non-optional field be hard or toggleable? Macros may wish to, say, automatically add it.
Future possibilities
This is quite a simple syntax change, so not much. Maybe some way to map attributes to multiple fields in a struct (e.g when serializing a struct composed with optional fields, add a #[serde(skip_serializing_if = "Option::is_none")]
). Alternatively, expanding on the auto-wrap of Some
, should there be more auto conversions in struct literals?