Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why isn't `From` automatically used for coercing to trait implementing type

I have two traits:

trait Foo {}
trait Bar {}

struct FooImpl;
impl Foo for FooImpl {}

struct BarImpl;
impl Bar for BarImpl {}

And a third type I want to convert into:

struct Baz;

trait IntoBaz {
    fn into(self) -> Baz;
}

I can't define two impls of IntoBaz for the two traits because of coherence, so I wrap one instead:

struct FooWrapper<F>(F)
where
    F: Sized;

impl<F: Foo + Sized> From<F> for FooWrapper<F> {
    fn from(f: F) -> FooWrapper<F> {
        FooWrapper(f)
    }
}

impl<F: Foo + Sized> IntoBaz for FooWrapper<F> {
    fn into(self) -> Baz {
        Baz
    }
}

And I don't wrap the other:

impl<B: Bar> IntoBaz for B {
    fn into(self) -> Baz {
        Baz
    }
}

fn do_thing<B: IntoBaz>(b: &B) {}

fn main() {
    do_thing(&BarImpl);
}

So far so good, but why doesn't this line work?

fn main() {
    do_thing(&FooImpl);
}

Motivation

I'm trying to add io::Write support to a library with fmt::Write support without introducing a breaking change.

The easiest way would be to define some internal Write trait which covers the shared behaviour, but the coherence problem means I can't just write From<io::Write> instances to the internal trait.

I've tried wrapping io::Write instances, to make the coercion explicit so the compiler prioritises the shorter path and avoids the incoherence, but it won't auto-coerce using the From instance.

like image 892
Isaac van Bakel Avatar asked Jan 01 '23 18:01

Isaac van Bakel


2 Answers

Look at the error message:

error[E0277]: the trait bound `FooImpl: Bar` is not satisfied
  --> src/main.rs:48:5
   |
48 |     do_thing(&FooImpl);
   |     ^^^^^^^^ the trait `Bar` is not implemented for `FooImpl`
   |
   = note: required because of the requirements on the impl of `IntoBaz` for `FooImpl`
note: required by `do_thing`
  --> src/main.rs:45:1
   |
45 | fn do_thing<B: IntoBaz>(b: &B) {}
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

It's saying that FooImpl doesn't have an implementation of Bar, which is a requirement of your blanket IntoBaz for B implementation.

The FooWrapper implementation is not relevant because FooImpl is not the same as FooWrapper. The From and Into traits provide a way to convert between types, but it doesn't happen automatically.

You might try adding an implementation for things that can be converted into FooWrapper, but this won't work because the implementations could overlap (and specialization is not stable yet).

But you can define an IntoBaz implementation for just FooImpl:

impl IntoBaz for FooImpl {
    fn into(self) -> Baz {
        IntoBaz::into(FooWrapper::from(self))
    }
}

Which will make your code compile:

fn main() {
    do_thing(&BarImpl);
    do_thing(&FooImpl);
}
like image 133
Peter Hall Avatar answered Jan 05 '23 17:01

Peter Hall


PeterHall's answer is completely correct about the question as asked. From and Into don't mean anything special on the type level.

However, you might just have asked the question too narrowly. It looks like you want do_thing(&BarImpl) and do_thing(&FooImpl) to compile and do the "right" things. If that's all you need, there is a somewhat tricky alternative approach that could work: add a type parameter to IntoBaz and use different types to make the impls non-overlapping.

trait IntoBaz<T> {
    fn into_baz(self) -> Baz;
}

struct ByFoo;
impl<F: Foo> IntoBaz<ByFoo> for F {
    fn into_baz(self) -> Baz {
        Baz
    }
}

struct ByBar;
impl<B: Bar> IntoBaz<ByBar> for B {
    fn into_baz(self) -> Baz {
        Baz
    }
}

do_thing can now be generic over T:

fn do_thing<T, B: IntoBaz<T>>(_: &B) {}

When you call it, if there is only one T that works, the compiler will find it automatically.

fn main() {
    do_thing(&BarImpl);
    do_thing(&FooImpl);
}

I'm trying to add io::Write support to a library with fmt::Write support without introducing a breaking change.

Unfortunately this suggestion is technically a breaking change. If there is some type that implements both io::Write and fmt::Write, then do_thing(&implements_both) (which used to use fmt::Write) will now fail to compile due to ambiguity. But any place where the choice of trait is unambiguous will still compile as it did before, so the risk of breakage is much lower.

See also:

  • Can I avoid eager ambiguity resolution for trait implementations with generics?
like image 27
trent Avatar answered Jan 05 '23 17:01

trent