Pre-RFC: Optional FIelds

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.