Pre-RFC: Optional FIelds

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.

You can't know what fields are missing from a macro. So this can't be implemented as a macro

You could actually do it in a macro in one of two ways:

  1. You could write a proc macro that generates a macro_rules macro from the struct definition. That's definitely doable, though the ergonomics around this are a lot worse than solution two:

  2. You could have a two part solution. You'd have a generic builder trait, write a derive for that trait, as well as a macro to invoke the builder trait for the struct. Then consumers slap on the derive and can use the macro when invoking the struct, which replaces struct field initializers with method calls on the builder. It's still a heavy solution (lots of generics and macros), and the ergonomics are still pretty bad (you'll get an error message about the builder struct, and macros inhibit IDE support and generics/macros increase the compile time). I used a variant of this approach in my rubber_duck crate to emulate named and default parameters for method calls.

Though you can do it in a macro I don't think people will try using it because of the overhead of using a macro.

Off-Topic-ish: Generally, I support the "try it as a macro first" attitude. But as the non-macro IDE experience gets better, macros come with a higher cost of not being IDE friendly, and I'm not sure how to fix that effect... I'd love some sort of way to hint to rust-analyzer and others tools what a macro usage expects both syntactically and type-wise, like a standard way to provide a grammar.... but that's also not something one can just prototype with a macro or casually test :frowning:

There are two major semantic problems here:

  • ? should primarily imply Result, not Option, and
  • ? acts like a monad bind, while here its simply a conversion.

The correct way to instantiate a struct without writing Some everywhere is to use .into().

I'd suggest exploring the = None draft with some ..} syntax that permits not fully initializing like Default requires. In fact, we could just aim for flexibility if we're going to do this:

/// Partial initialization of struct-like `struct`s and `enum`s
///
/// Rust magically remembers which members `partial_default`   
/// initializes which members it does not initialize.
/// If `T: PartialDefault` then you can initialize `T` like
/// `T { foo_1, .., foo_n, ... }` provided that `foo_1, .., foo_n` cover
/// all members not initialized by `T: PartialDefault`.
///
/// In this example, Rust initializes `foo_1, .., foo_n` before invoking
/// `partial_default`, so `partial_default` could inform its initializing
/// assignments by reading `foo_1, .., foo_n`.   In particular, if `T` is
/// an `enum` then Rust sets the `enum` variant before invoking
/// `partial_default`, so that  `partial_default` knows which variant it
/// constructs.  `#[derive(Default)]` invokes `partial_default` if available.
///
/// Inside `partial_default`, we caution against creating a `&mut Self`
/// via `let self = unsafe { self.assume_init_mut() }`.  Instead we
/// suggest creating a `*mut Self` with `let self = self.as_mut_ptr();`
/// from which you project and write to individual fields, like
/// ```
/// unsafe { core::ptr::write(
///     addr_of_mut!((*self).field),  // Please note &mut (*self).field as *mut FieldType, risks UB
///     value
/// ) };
/// ```
/// This handles `Drop` fields correctly.
///
/// Only supports struct-like `struct`s and `enum`s, not tuple-like,
/// and never other types.  We mark `partial_default` unsafe because
/// it initializes drop fields without dropping their existing contents.
/// In particular `PartialDefault` cannot "reset" types unless `Self: Copy`.
unsafe trait PartialDefault {
    unsafe fn partial_default(self: &mut MaybeUninit<Self>);
}

In this way, you could specify remarkably flexible behaviors with manual unsafe impl PartialDefault for MyType, while also supporting proc macros that performed more convenient initialization. Rust could provide = None as a special case for Option and perhaps = more generally.

1 Like

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.