Mini pre-RFC: Make `Into` trait object safe


#1

Summary

Change the definition of Into trait such that it does not require Self: Sized.

pub trait Into<T> {
    /// Performs the conversion.
    fn into(self) -> T;
}

Motivation

As we don’t have #[feature(unsized_locals)] before, we don’t want to allow dyn Into<T> because it will be useless.

Once we allow object safe methods to receive self by value, the reason above disappears.

Enabling dyn Into<T> on the other hand, enables a lot of new abstractions to appear, and it can be seen as Rust’s standard representation of lazy evaluations. For example, we can image the short circuit boolean operation being defined as

trait And<Rhs=Self> {
    type Output;
    fn and(self, rhs: dyn Into<Rhs>) -> Self::Output;
}
impl And for bool {
    type Output = bool;
    fn and(self, rhs: dyn Into<bool>) -> bool {
        if self { return true } else { rhs.into() }
    }
}

(The above is not implied in this RFC, only demonstrates future improvement possibilities).

(However, many other traits in std::ops can be relaxed like this, I am not sure shall I included them as well.)

(Add/Div/... were already relaxed…)


#2

This would require unsized rvalues, which is currently in the works. There is no other way to make this change, because changing the signature of Into would be a breaking change. Currently this is impossible to do.


#3

Yes this is what I am going to say. But is this already in stabilizing?


#4

Yes, it has been accepted, but right now it isn’t implemented, so it may take a while to get it. Also just because a RFC gets accepted, doesn’t mean it will get stabilized (see placement new). That said, I doubt unsized rvalues will be unaccepted.


#5

I just noticed this was already implemented.

#![feature(unsized_locals)]
...
let v:Box<dyn FnOnce()> = Box::new(||{});
v()

is now working.

I also have issued a bug regarding miri.


#6

This is coercions at work, not the same as unsized rvalues.

With unsized rvalues I could do

let f: dyn FnOnce() = || ();
f();

Which is currently illegal.

The related traits for coercion are std::ops::CoerceUnsized and std::marker::Unsize


#7

Ok. But as long as we know that into is callable when dyn Into do exist, it is still already possible to use dyn Into right? So I didn’t see a reason to stop this. Maybe the And example I gave is too early and require something else, but making dyn Into usable is already implemented.


#8

I’m not trying to stop this (I probably could have communicated that better), I just want it noted that this pre-RFC will depend on something which is currently not even implemented (unsized rvalues). Also, currently it would not be possible to call into if we had a dyn Into because Self is dyn Into, which is !Sized. !Sized values cannot be used on the stack, because their size is unknown at compile time and because of this we cannot initialize the self parameter of into. Due to this, it is impossible to call into without unsized rvalues (which will allow !Sized values on the stack) or a change to into's signature (which not an option due to stability concerns).


#9

What do you mean by this? (I don’t see any And example)

–edit–

I missed the example in the pre-RFC while trying to find it :sweat_smile:


edit 2

It is already possible to implement this in another way

impl<Rhs: Into<bool>> And<Rhs> for bool {
    type Output = bool;
    fn and(self, rhs: Rhs) -> bool {
        if self { return true } else { rhs.into() }
    }
}

#10

In my top post I have a example that defines a And trait.

I can confirm though, if I simply define a new trait

Playground

trait NewInto<T> {
    fn new_into(self) -> T;
}
impl<T,I:Into<T>> NewInto<T> for I {
    fn new_into(self) -> T {
        self.into()
    }
}

fn main() {
    let b:Box<dyn NewInto<i32>> = Box::new(10);
    println!("{}", (*b).new_into())
}

It is just working.

Oh. Maybe I would need better example then.


#11

Oh, ok. Then it is implemented, at part of it.

Yes

As I showed before, this problem can be solved with normal traits and generics.

i.e.

trait And<Rhs = Self> {
    type Output;
    
    fn and(self, other: FnOnce() -> Rhs) -> Self::Output;
}

impl And for bool {
    type Output = bool;
    
    fn and(self, other: FnOnce() -> bool) -> bool {
        if self { other() } else { false }
    }
}

and the desugaring of && could be

a && b

turns into

And::and(a, || b);

Also isn’t removing a trait bound a breaking change? Would that apply in this case (because Sized is auto implemented)?


#12

I am not sure. But I think it is not likely, as we don’t have negative trait bounds yet.

I think for the type T it is implied to be Sized, but not for Self. However, the source code explicitly requires Self: Sized and this is what I want to change.


#13

Negative trait bounds would make adding a trait a breaking change. Removing a trait bound for a trait could be a breaking change. For example say (in some crazy world) we remove the trait bound Clone for Copy. Any code that relies on the fact the every type that implements Copy also implements Clone would break. But with the Sized trait I’m not so sure because it is the default and it is auto-implemented.


#14

For traits the default is ?Sized, but you can transitively infer Sized.

This is contrived, but here I’m able to depend on Into: Sized to call uninitialized():

trait Foo: Into<()> {
    fn foo() {
        let x: Self = unsafe { std::mem::uninitialized() };
        std::mem::forget(x);
    }
}

So yes, I think removing a Sized trait bound is technically a breaking change, though I have no idea if this would come up in a more realistic scenario.