Pre-RFC: Optional FIelds

This proposes both None-ellision and Some-wrapping. Is there any reason they couldn't be separate?

If this was implemented as proposed, you couldn't look at a struct expression containing no FRU and conclude that all the fields were listed. Have you considered an explicit syntax to enable None-ellision? E.g.

User? { username: "abc123", }
// n.b. this could just be an extension to the referenced Default RFCs,
// and not require a new form of struct field declaration

Along those same lines, you could no longer look at field_name or field_name: value and be sure it's not an Option anymore. (E.g. the only part of the OP visible to me right now contains user_name: "abc123", and I cannot recall if this is one of the optional fields or not.) Why not signal the Some-wrapping with the new suffix instead?

User { full_name?: "Beatrix Kiddo", /* gets `Some`-wrapped */  ... }
User { full_name: None, /* works like today */ ... }

Am I missing something when I think the above two changes (and dropping the new form of struct field declaration) would make everything a local syntactical effect? I feel it's better to avoid "action at a distance" where one has to refer back to the declaration to be able to reason about what's going on.

Apologies for the two consecutive posts, but I recalled something else I meant to mention.

This is inconsistent with how unit-versus-empty structs work today. This may be due to unit structs occupying both the value namespace and type namespace, like tuple struct constructors (though I'm not 100% certain of that). So it's possible your suggestion is a breaking change or otherwise non-trivial to implement.

struct Unit;
struct Empty {}
    
// These work
let unit = Unit;
let unit = Unit {};
let empty = Empty {};

// This does not
// let empty = Empty;

// Nor does this
// #[allow(non_snake_case)]
// let Unit = "foobar";

// But this does
#[allow(non_snake_case)]
let Empty = "foobar";

Given that you have fully defined the type I would have assumed that it is None since the other option isn't a valid Option<u32>.

Do you have concrete/real-world examples for how "messy" and "out of hand" this gets?

1 Like

That's a pretty weak motivation. While the RFC saves characters, one should be very careful about adding one-off syntax changes like this.

For one, shortness doesn't necessarily help reading. (Ease of reading the code is far more important than writing convenience – code is read a lot more than it is written.) In this case, I find the proposed syntax harder to read than Option. Rust already has a proliferation of symbols, I'm glad at least most types don't hide behind sigils (well, except pointers and references, which are builtins, unlike Option). Adding yet another overload for the meaning of ? would be a mistake.

Second, the proposal doesn't carry its weight when put in the context of other parts of the language, either. Why just struct fields? Option can be used in any place where a type is expected. To me, this just signals that the idea is not really fleshed out, and it's not considerate nearly enough with regards to its impact on and interaction with everything else.

Finally, there's also the question of priority. There far more important things in Rust development to worry about. The compiler wants its soundness bugs fixed, const generics and variadic generics are desperately needed, specialization has a fair number of unresolved correctness questions, the list goes on. It is especially ill-advised to push for superficial changes like this when the design, implementation, and testing of far more substantial features still somewhat lacks sufficient human resources.

If you want a syntax tweak, just use a macro. That's exactly what macros are for. You can write a procedural macro that transforms the proposed code into the currently-accepted style. It's better for you too, because you don't have to wait for it. And it's better for the community, because tools and libraries that operate over Rust syntax (e.g. rustfmt, syn, all (!!!) procedural macros, etc.) don't have to update their syntax and AST support code with yet another case.

There are countless syntax changes that one could reasonably propose. The question is always "why?" and not "why not?" – if all of these changes would be accepted and implemented, the developers would do nothing else other than changing the parser all the time. I'd therefore argue that whatever syntax can reasonably be implemented as a macro should in fact be a macro and not a core language change.

6 Likes

How would this argument evolve if default field values existed in general? For example, I could imagine that a configuration struct might no longer have a bunch of Nones, like one could be

struct DeflateOptions {
    level: CompressionLevel = CompressionLevel::Balanced,
    dictionary_size: usize = 1 << 15,
    word_size: usize = 32,
}
5 Likes

One of my test cases for a library I'm working on (the structure has to be this way, so it's partly a special case, but there are many structs that could be ported from builders)

Predicate::EntityProperties {
    entity: PlayerContextEntity::This,
    predicate: EntityPredicate {
        distance: Some(DistancePredicate {
            horizontal: Some(Range {
                min: 0.0,
                max: 10.0,
            }),
            ..default()
        }),
        equipment: Some(EquipmentPredicate {
            mainhand: Some(ItemPredicate {
                count: Some(OptionalRange::Exact(32)),
                ..default()
            }),
            ..default()
        }),
        ..default()
    },
}
1 Like

That's a pretty weak motivation. While the RFC saves characters, one should be very careful about adding one-off syntax changes like this.

That quote was from a tiny note about using .into() instead of Some(), so that's taken out of context quite a bit. And while it's true that this RFC does save characters, that's not the only motivation listed.

Second, the proposal doesn't carry its weight when put in the context of other parts of the language, either. Why just struct fields? Option can be used in any place where a type is expected.

Potentially, something that could go into the future section, though I don't think it aligns with this particularly, since it's all a special case for structs.

To me, this just signals that the idea is not really fleshed out, and it's not considerate nearly enough with regards to its impact on and interaction with everything else.

Not necessarily. I think this deserves its own syntax, given how complex some structs can be, especially if it would mean people could port the builder pattern afterwards.

Finally, there's also the question of priority. There far more important things in Rust development to worry about. The compiler wants its soundness bugs fixed, const generics and variadic generics are desperately needed, specialization has a fair number of unresolved correctness questions, the list goes on. It is especially ill-advised to push for superficial changes like this when the design, implementation, and testing of far more substantial features still somewhat lacks sufficient human resources.

I don't think this is a valid argument at all, similar to saying "why explore space when we have problems here on earth"? Yes, there are some problems in the compiler, and const generics are needed, but that shouldn't mean not adding new features, e.g the try block, which is (mostly) equivalent to an immediately invoked closure. But, calling it try is much nicer, signifies intent clearer, and does in fact save characters.

If you want a syntax tweak, just use a macro. That's exactly what macros are for. You can write a procedural macro that transforms the proposed code into the currently-accepted style. It's better for you too, because you don't have to wait for it. And it's better for the community, because tools and libraries that operate over Rust syntax (e.g. rustfmt , syn , all (!!!) procedural macros, etc.) don't have to update their syntax and AST support code with yet another case.

There's no reasonable way a macro could figure out if a field was flagged optional or not, any implementation of such a macro would be rather buggy.

1 Like

Sorry, I don't really understand the question. As in automatically wrapping other types that are defaulted? Like auto-boxing, or auto .intoing? Potentially, though of course we don't want to be too implicit. With Options alone, it's obvious what the conversion will be; with other types, potentially less so.

Unless the question is "if we get default field values, will we need to use options everywhere anymore?". That's a good point to bring up, and certainly one to consider.

Hmm. Ideally, the ? would appear after the value, but a try operator could potentially go there too. In a struct declaration, I think it going after the type makes more sense though, since after all it is wrapping the type. Also the different positions signify different things this way.

That's the direction I meant, yeah.

(Also part of what I was implying with "how that RFC changes the way people write code" in #4.)

Interesting, that was something I didn't really think through.

This doesn't really work for tuple structs:

Foo(None) // where should the question mark go?

My biggest concern however it's that this is a special case, and it's not immediately clear why &str? can only be used in structs. But allowing it elsewhere too might be undesirable, e.g. if we support it in function parameters, the "None-elision" would introduce optional function parameters. By allowing it in the function return type, we would get Some-wrapping, similar to Ok-wrapping in try blocks. And allowing it in local variables has some interesting consequences for type inference:

let x: i32? = 5; // removing the type annotation alters the code's behavior!

I realize that the RFC only proposes Option sugar in structs, but when that sugar is implemented, it seems logical to me to make it available elsewhere as well (like impl Trait, which is becoming legal in more and more places). That would be more consistent and match the behaviour of TypeScript or Kotlin more closely.

So I would prefer a syntax that is both more general and more explicit. If we add T? syntax sugar for Option<T>, it should be available pretty much everywhere. Null-elision should be opt-in:

struct Foo {
 bar: i32? = None,
}

I'm also not sure if Some-wrapping is a good idea. Having Some-wrapping but not None-elision doesn't make much sense, because then None would be ambiguous if the type is Option<T>?.

You can already get Some-wrapping in function parameters:

fn foo(bar: impl Into<Option<i32>>) {}

Unfortunately, this can cause code bloat because the function is monomorphised multiple times. Having Some-wrapping might be better in this case.

1 Like

This doesn't really work for tuple structs:

It explicitly mentions that tuple structs don't work with this.

My biggest concern however it's that this is a special case, and it's not immediately clear why &str? can only be used in structs.

Yep, it's definitely a special case, though you do go on to say why, i.e

But allowing it elsewhere too might be undesirable, e.g. if we support it in function parameters, the " None -elision" would introduce optional function parameters.

I realize that the RFC only proposes Option sugar in structs, but when that sugar is implemented, it seems logical to me to make it available elsewhere as well (like impl Trait , which is becoming legal in more and more places). That would be more consistent and match the behaviour of TypeScript or Kotlin more closely.

Possibly, yes, this could be done when this is implemented.'

Thanks for looking over it, I'll add some of your examples and considerations into it

Let me rephrase that: It would be surprising to Rust users that it only works for structs, so people would likely want to extend it to other context, so we should ask ourselves now if we want to go down that road :slightly_smiling_face:

Oh ok

That's simply not true. A procedural macro has access to the exact token stream of its input. If the compiler can parse a field annotated with ?, then so can a proc-macro.

That's...not at all what I was saying. I understand that macros have access to the token input. What they don't have access to is the context. If I have the example User struct, how on earth is a macro supposed to know, in a struct literal, what the definition and thus option-ness is of the types?

If you mean implicitly Some()-wrapping fields: that's another can of worms (and IMO very undesirable), but the macro expansion could always just add .into() or From::from() unconditionally since there's a From<T> impl for T as well as Option<T>.

1 Like

This would break type inference in many cases.