More magics on Option<T>?

There are postponed RFCs for &move. The idea of this is to let the function receives a &move T reference have the right to drop the object.

If T is Sized, there is a way of doing it in today’s Rust:

fn drop_a_value<T>(obj: &mut Option<T>)
where
    T: Sized
{
    *obj = None;
}

This does not help when we want to use trait objects, because they are not Sized.

So I am thinking instead of introducing something like &move, we can actually make &mut Option<dyn Trait> a proper trait object, and allow it to be a method receiver:

impl MyTrait for Whatever {
    fn my_method(self: &mut Option<Self>,...) ...

...
}

Of cause, this method will need to check whether self is Some otherwise it will panic. But the magic here is that when we write the following:

let foo: &mut Option<dyn Foo> = &mut Some(ConcretFoo::new());

we first construct an object of type ConcretFoo, and then create a Option<ConcretFoo> variant Some. Then construct a vtable from ConcretFoo's Foo implementation, to put in the fat pointer foo.

If we write

let foo: &mut Option<dyn Foo> = &mut (None as Option<ConcretFoo>)

It will create a None variant, and then use the vtable for ConcretFoo for the fat reference. Technically this is a “bare” vtable for a specific trait implementation, because it does not point to a real object.

The last case is

let foo: &mut Option<dyn Foo> = &mut None;

Intuitively, this will use the implementation of Foo for !, because there is no further constraint on the concret type can exist except that it must implement Foo.

All above should be the same when use assignment. So the concret type the trait object points to will vary.

For consistancy, I would like to see &Option<dyn Trait> can do the same thing.

What about RFC 1909? My understanding of it is that once it’s implemented it’ll effectively be &move for unsized values.

There’s also the small detail that Option does not declare T: ?Sized. Maybe what you want is Option<Box<dyn Foo>>, whose contents can be destroyed with your drop_a_value function?

1 Like

Mentioning &move is not the main interest of this idea. I am more interested in making &mut Option<dyn T> a proper trait object.

Once we have this, we can have something asked frequently by different language users, includes but not limited to C++/Java/C#, and Rust - The so called “static virtual function”. That said, we can now put “static” methods (methods without receiver) to the vtable as well as other usual vtable functions. The only thing we need is to derive a method with receiver &mut Option<Self>, and delegate to the static method, so it will work properly even when the user passes a None of a concert type.

This feature was asked and almost always receives “NO” as answer in all languages I saw, and the workaround are always something like singleton/monostate patterns.

  1. What would a “static virtual function” involving &mut Option<Self> even do? What’s an example of a use case for it? The way you just described it, it sounds like a function that acts like an ordinary method if called from a method, and does nothing if called from anywhere else. I must be missing something huge since that doesn’t make any sense.

  2. Are you sure there really are users asking for this functionality? In C++/Java/C#, every time I’ve seen someone ask to do more things in static functions, they really just didn’t understand what a static function was in the first place (e.g., they were also the kind of novice that asks “why can’t static functions call non-static functions?” as if it’s not a tautology). Especially in Java/C# where the lack of free functions encourages such confusion.

It sounds like what @earthengine wants is the ability to make static/free function calls dynamic over a trait implementation. They want a vtable that doesn’t take a self. This can be done with generics or with ZST receivers.

You could argue that the ZST receiver is wasteful here, because the size of the &dyn Trait is (ptr: usize, vtable: usize) where the actual pointer is never used, just the vtable. However, I don’t see how &mut Option<dyn Trait> as a receiver would change anything.

OK. I will explain.

The idea of “static virtual function” is to do dynamic dispatch depending on the concrete type, not the value. A classic example is the “virtual constructor”: You want to create an instance of a Pet, but until runtime (the user selection) you don’t know whether you should create a Cat or a Dog.

In all languages I mentioned including Rust, this is done by introducing a Factory pattern.

But with my proposal, we can write

trait Pet{
    //derive a method for
    //fn new_pet(self: &mut Option<Self>);
    // and mark the original method out of the vtable
    #[derive(option_factory)]
    fn new_pet() -> Self;
}

struct Dog;
impl Pet for Dog {
    fn new_pet() -> Dog {
       Dog
    }
}

struct Cat;
impl Pet for Cat {
    fn new_pet() -> Cat {
        Cat
    }
}

fn set_pet(pet_holder: &mut Option<dyn Pet>) {
    //I wanted to say this
    //pet_holder.new_pet()
    //but I think this is more intuitive although requires more compiler magic
    *pet_holder = Some(pet_holder.new_pet());
}
fn choose_from_menu(options: Vec<&mut Option<dyn Pet>>) -> &mut Option<dyn Pet> {
     options[0]
}

fn main() {
    let cat = None as Option<Cat>;
    let dog = None as Option<Dog>;
    let pets = vec![&mut cat, &mut dog];
    let pet = choose_from_menu(pets);
    set_pet(pet);
}

//the derived method
fn new_pet_derived(self: &mut Option<Self>) {
    //Simply delegate to the static method
    //After monomorphization this should use the method from the concrete  type
    *self = Some(Self::new_pet());
}

ZSTs can help on making static functions virtual, in other languages this is called the Monostate pattern (or slightly different, Singleton, which can have internal states whilst Monostate is constant).

The problem is that they require all methods of a specific type to be static or object state irrelevant. In other words, I want to put some object state aware methods as well as the irrelevant ones, and put this fact in the method signature, so the caller only need to check the signature to ensure the implementation does not even refer to the object data.

Also, I never see that the unused pointer field for ZST being a problem. Instead, I want to USE this field as a pointer to work on, without assuming an object is already initialized (this is the case when &mut Option<dyn Trait> points to a None).

Okay, this sort of use case I'm familiar with, but I don't see why any Option magic is required or even beneficial for it. You can already statically dispatch on the type with a generic constructor like fn<T: Pet> new_pet() -> T { return T::new(); }, or "dynamically dispatch" on far more ordinary values like an enum PetType { Cat, Dog } or bool: must_support_barking or whatever. Why do you want this new magic Option style of virtual constructor?

The reason not doing with an enum or bool etc, is of cause trait is open and enum is closed. One can implement another type of strange Pet

struct Spider{}
impl Pet for Spider{...}

without having to change the crate that defines Pet (as an enum).

Yes, in type theory, traits (viewed as “intersection type”) are very close to an enum (sum type). But for code organization (software engineering) they serves for different purposes.

What about static dispatch?

Static dispatch is not in consideration. I know it is possible already, but it is not “dynamic” enough.

It seems like an alternative that avoids doing anything special with Option would be some form of “class-safe” traits (to borrow the corresponding OOP to “object-safe” traits). Taking your earlier example it could be something like:

trait Pet {}

// "class-safe" because it only has static methods that don't return `Self`
trait NewPet {
    fn new() -> Box<dyn Pet>;
}

struct Dog;
impl Pet for Dog {}
impl NewPet for Dog { fn new() -> Box<dyn Pet> { Box::new(Dog) } }

struct Cat;
impl Pet for Cat {}
impl NewPet for Cat { fn new() -> Box<dyn Pet> { Box::new(Cat) } }

fn choose_from_menu(options: Vec<class NewPet>) -> class NewPet {
     options[0]
}

fn main() {
    let pets = vec![classof(Dog), classof(Cat)];
    let pet_type = choose_from_menu(pets);
    let pet: Box<dyn Pet> = pet_type.new();
}

classof(Dog) would be something like a ZST for the struct type, coerced to class NewPet would be a ZST + vtable of the NewPet static methods.

Presumably there’s some much more appropriate terminology and syntax for this, and I’m not sure if anything like this is worthwhile, this was just my first thought on seeing your usecase.

EDIT: Looking at this again I realise it’s basically the factory pattern with some sugar. I guess part of what you want is to combine the Pet and NewPet traits into one? Which would require supporting slicing off the “class-safe” parts of the trait when defining the class vtable.

Which would require supporting slicing off the “class-safe” parts of the trait when defining the class vtable.

This is already exists in Rust. Example:

trait NotTraitObj {
    fn do_something(&self);
    fn new() -> Self;
    fn do_somethingelse(&self);
}
trait TraitObj {
    //in vtable
    fn do_something(&self);
    //not in vtable
    fn new() -> Self where Self: Sized;
    fn do_somethingelse(&self) where Self:Sized;
}
struct MyObj();
impl TraitObj for MyObj {
    fn new() -> Self where Self: Sized {
        MyObj()
    }
    fn do_something(&self){}
    fn do_somethingelse(&self){}
}
impl NotTraitObj for MyObj {
    fn new() -> Self {
        MyObj()
    }
    fn do_something(&self){}
    fn do_somethingelse(&self){}
}

fn main() {
    let obj = MyObj();
    let to: &TraitObj = &obj;
    to.do_something();
    //error: the `do_somethingelse` method cannot be invoked on a trait object
    //to.do_somethingelse();
    //error[E0038]: the trait `NotTraitObj` cannot be made into an object
    //let nto: &NotTraitObj = &obj;
}

In short, if you declare a method with constraint Self: Sized, this method is not "virtual" and will not be in the vtable.

I didn't use this trick in my code above because 1) I am demostracting hyperthetical grammar; 2) I want to make it callable on trait objects.

Another case that &mut Option<dyn Trait> might be useful is to handle the Box<FnOnce> issue.

fn main() {
    let c = ||{};
    let b :Box<dyn FnOnce()> = Box::new(c);
    b()
}

Today the above results in E0161: cannot move out of box. But we can image to have compiler support for desugaring it to:

fn main() {
    let c = ||{};
    let b:Box<dyn FnOnce()> = box c;
    //this only copies the vtable, and move the destructor from b to the place holder
    let _place_holder:&mut Option<dyn FnOnce()> = unbox_to_some(b);
    //fn call_once_derived(self: &mut Option<dyn FnOnce()>) 
    //{ (self.take().unsafe_unwrap())() }
    _place_holder.call_once_derived()
}

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