Trait that automatically implies several From implementations?

I would like to be able to concisely write that a generic type implements a couple of From traits. Is this possible (in stable or unstable Rust)? My current attempts (which weren't very successful), are as follows:

trait Versatile
where
    Self: Sized,
    String: From<Self>,
    i64: From<Self>,
    i32: From<Self>,
    i16: From<Self>,
{
}

#[derive(Clone, Copy)]
struct Dummy;

impl From<Dummy> for String {
    fn from(_: Dummy) -> Self {
        "Dummy".to_string()
    }
}

impl From<Dummy> for i64 {
    fn from(_: Dummy) -> Self {
        42
    }
}

impl From<Dummy> for i32 {
    fn from(_: Dummy) -> Self {
        42
    }
}

impl From<Dummy> for i16 {
    fn from(_: Dummy) -> Self {
        42
    }
}

impl Versatile for Dummy {}

fn foo<D>(mut v: Vec<D>)
where
    D: Versatile,
    // Why do I need the following bounds?
    // Aren't these implied by `Versatile`?
    String: From<D>,
    i64: From<D>,
    i32: From<D>,
    i16: From<D>,
    /* imagine many more bounds here */
{
    let s = String::from(v.pop().unwrap());
    let big = i64::from(v.pop().unwrap());
    let medium = i32::from(v.pop().unwrap());
    let small = i16::from(v.pop().unwrap());
    println!("{}, {}, {}, {}", s, big, medium, small);
}

// I can try to workaround:
trait Workaround: Into<String> + Into<i64> + Into<i32> + Into<i16> {}
impl<T: ?Sized> Workaround for T where
    T: Into<String> + Into<i64> + Into<i32> + Into<i16>
{
}

fn bar<D>(mut v: Vec<D>)
where
    // I can now write more concise:
    D: Workaround,
{
    // But then I can't use `From::from` anymore:
    /*
    let s = String::from(v.pop().unwrap());
    let big = i64::from(v.pop().unwrap());
    let medium = i32::from(v.pop().unwrap());
    let small = i16::from(v.pop().unwrap());
    */
    // Instead I am *required* to use `Into::into`:
    let s: String = v.pop().unwrap().into();
    let big: i64 = v.pop().unwrap().into();
    let medium: i32 = v.pop().unwrap().into();
    let small: i16 = v.pop().unwrap().into();
    println!("{}, {}, {}, {}", s, big, medium, small);
}

fn main() {
    foo(vec![Dummy; 4]);
    bar(vec![Dummy; 4]);
}

(Playground)

Output:

Dummy, 42, 42, 42
Dummy, 42, 42, 42

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 1.21s
     Running `target/debug/playground`


P.S.: I had a similar problem before, see: Generic impl TryFrom impossible due to orphan rules – what should I do? Maybe this one is very related to it, though I already "gave up" on hoping for default implementations (as needed/wanted in the previous thread). Right now, I'd be happy if I have to do all From implementations manually (or via macro), as long as I can have a single trait to describe that all of these are supported.

You currently have to use Into, as in your "workaround". The Rust compiler doesn't currently understand that a trait bound should imply other bounds unless those bounds have Self as the left side (or, equivalently, are written with "supertrait" syntax). I think this is the issue for that:

https://github.com/rust-lang/rust/issues/20671

2 Likes

See also implied bounds, which is dependent on Chalk it seems.

Thanks for your hints. In the given case I decided to resort to use Into as a workaround.

However, I didn't take long until I ran into another (related) problem, and in the second case, the compiler error is kinda disturbing (and giving a wrong hint).

The following code is demonstrating the problem:

trait MaybeStr
where
    for<'a> &'a Self: TryInto<&'a str, Error = ()>,
{
    fn try_as_str(&self) -> Result<&str, ()> {
        self.try_into()
    }
}

fn foo<T>(value: T) -> Result<String, ()>
where
    T: MaybeStr,
    // uncomment this, and everything works:
    // for<'a> &'a T: TryInto<&'a str, Error = ()>,
{
    // Note: I don't even use `.into()` here!
    let s: &str = value.try_as_str()?;
    Ok(s.to_string())
}

enum Datum {
    Str(String),
    Num(f64),
}

impl<'a> TryFrom<&'a Datum> for &'a str {
    type Error = ();
    fn try_from(datum: &'a Datum) -> Result<&'a str, ()> {
        match datum {
            Datum::Str(s) => Ok(s),
            _ => Err(()),
        }
    }
}

impl MaybeStr for Datum {}

fn main() {
    assert!(Datum::Num(0.0).try_as_str().is_err());
    println!(
        "{}",
        Datum::Str("It works!".to_string()).try_as_str().unwrap()
    );
    println!(
        "{}",
        foo(Datum::Str("This also works!".to_string())).unwrap()
    );
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0271]: type mismatch resolving `for<'a> <&'a T as TryInto<&'a str>>::Error == ()`
  --> src/main.rs:12:8
   |
12 |     T: MaybeStr,
   |        ^^^^^^^^ expected `()`, found enum `Infallible`
   |
note: required by a bound in `MaybeStr`
  --> src/main.rs:3:40
   |
1  | trait MaybeStr
   |       -------- required by a bound in this
2  | where
3  |     for<'a> &'a Self: TryInto<&'a str, Error = ()>,
   |                                        ^^^^^^^^^^ required by this bound in `MaybeStr`

error[E0277]: the trait bound `for<'a> &'a str: From<&'a T>` is not satisfied
  --> src/main.rs:12:8
   |
12 |     T: MaybeStr,
   |        ^^^^^^^^ the trait `for<'a> From<&'a T>` is not implemented for `&'a str`
   |
   = note: required because of the requirements on the impl of `for<'a> Into<&'a str>` for `&'a T`
note: required because of the requirements on the impl of `for<'a> TryFrom<&'a T>` for `&'a str`
  --> src/main.rs:26:10
   |
26 | impl<'a> TryFrom<&'a Datum> for &'a str {
   |          ^^^^^^^^^^^^^^^^^^     ^^^^^^^
   = note: required because of the requirements on the impl of `for<'a> TryInto<&'a str>` for `&'a T`
note: required by a bound in `MaybeStr`
  --> src/main.rs:3:23
   |
1  | trait MaybeStr
   |       -------- required by a bound in this
2  | where
3  |     for<'a> &'a Self: TryInto<&'a str, Error = ()>,
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `MaybeStr`
help: consider extending the `where` bound, but there might be an alternative better way to express this requirement
   |
12 |     T: MaybeStr, &'a str: for<'a> From<&'a T>
   |                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Some errors have detailed explanations: E0271, E0277.
For more information about an error, try `rustc --explain E0271`.
error: could not compile `playground` due to 2 previous errors

Following the compiler's advice to add &'a str: for<'a> From<&'a T> is also syntactically wrong, because:

 fn foo<T>(value: T) -> Result<String, ()>
 where
     T: MaybeStr,
+    // This is what the compiler suggests:
+    &'a str: for<'a> From<&'a T>

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0261]: use of undeclared lifetime name `'a`
  --> src/main.rs:14:6
   |
10 | fn foo<T>(value: T) -> Result<String, ()>
   |        - help: consider introducing lifetime `'a` here: `'a,`
...
14 |     &'a str: for<'a> From<&'a T>
   |      ^^ undeclared lifetime

For more information about this error, try `rustc --explain E0261`.
error: could not compile `playground` due to previous error

If you keep following the compiler's advice, things (obviously) won't get any better: (Playground), (Playground), or (Playground).

So the compiler obviously does some bad stuff here. (Note that this is all stable Rust.) Does anyone of you know if there's an issue on that behavior already? (I'll likely have a look myself and open an issue if I don't find anything.)

Also, is there any good workaround that doesn't involve writing an additional for<'a> &'a T: TryInto<&'a str, Error = ()> bound each time I want to use MaybeStr::try_as_str()?

That syntax is really verbose and even more verbose in the original code where I encountered the error. The example given here is a simplified version of what I actually need.

Also note the thread Generic impl TryFrom impossible due to orphan rules – what should I do?, which I linked above. I guess I could use @steffahn's "advanced trait trickery" here, but not sure if it works with references too, and even if it did work, not sure if I really want to go that path of using dark magic… I guess I could use macros too, but that's also kinda ugly.

Your main application of the supposed-to-be-a-supertrait bound here seems to be the default implementation. You can use a generically implemented trait to get basically the same effect:

trait CanImplementMaybeStr {
    fn try_as_str_implementation(this: &Self) -> Result<&str, ()>;
}
impl<T: ?Sized> CanImplementMaybeStr for T
where
    for<'a> &'a Self: TryInto<&'a str, Error = ()>,
{
    fn try_as_str_implementation(this: &Self) -> Result<&str, ()> {
        this.try_into()
    }
}

trait MaybeStr: CanImplementMaybeStr {
    fn try_as_str(&self) -> Result<&str, ()> {
        Self::try_as_str_implementation(self)
    }
}

fn foo<T>(value: T) -> Result<String, ()>
where
    T: MaybeStr,
{
    let s: &str = value.try_as_str()?;
    Ok(s.to_string())
}

enum Datum {
    Str(String),
    Num(f64),
}

impl<'a> TryFrom<&'a Datum> for &'a str {
    type Error = ();
    fn try_from(datum: &'a Datum) -> Result<&'a str, ()> {
        match datum {
            Datum::Str(s) => Ok(s),
            _ => Err(()),
        }
    }
}

impl MaybeStr for Datum {}

fn main() {
    assert!(Datum::Num(0.0).try_as_str().is_err());
    println!(
        "{}",
        Datum::Str("It works!".to_string()).try_as_str().unwrap()
    );
    println!(
        "{}",
        foo(Datum::Str("This also works!".to_string())).unwrap()
    );
}

You don't get to use the TryInto implementation directly anymore when using this trait, but that might not be relevant to you? Or maybe it is, in which case, feel free to add a use-case of that to the example. In any case, it's hard to argue about these examples because they seem rather artificial toy examples. Unless they're supposed to stand-in for more-lengthy-but-more-reasonably code.

Here is part of my real code (actually example code of the documentation I'm working on). But this might actually be more confusing, and I don't need help in fixing the particular code yet. But since you asked, here it is:

use std::rc::Rc;
use std::cell::RefCell;
use my_sandboxing_system::prelude::*;
use my_sandboxing_system::{Callback, Compile, Machine};

fn collect_output<'a, 'b, M, C>(
    machine: &'b M,
    setup: C,
    run: C,
) -> Result<String, MachineError>
where
    M: Machine<'a> + Compile<'a, C> + Callback<'a>,
    for<'c> <M as Machine<'a>>::Datum<'b, 'c>: MaybeString<'c>,
    // I want to avoid this:
    for<'c, 'd> &'d <M as Machine<'a>>::Datum<'b, 'c>: TryInto<&'d str, Error = MachineError>,
{
    let output_cell = RefCell::new(String::new());
    let output_rc = Rc::new(output_cell);
    let output_weak = Rc::downgrade(&output_rc);

    let my_print = machine
        .callback_1arg(move |s| {
            output_weak
                .upgrade()
                .ok_or("expired")?
                .borrow_mut()
                // but use `try_into`,
                // as I think that's most idiomatic, isn't it?
                .push_str((&s).try_into()?);
            Ok(vec![])
        })?;

    machine.compile(None, setup)?.call([my_print])?;
    machine.compile(None, run)?.call([])?;

    Ok(Rc::try_unwrap(output_rc).unwrap().into_inner())
}

Also note my Problem with HRTBs and associated type bounds in that matter.

I feel like I should remember the wisdom I found once.

Thank you a lot for showing me that workaround, I'll consider using that, but I'm currently tempted to simply do the following:

/// Types that can be an UTF-8 text string
pub trait MaybeString<'c>
where
    Self: From<String>,
    Self: From<&'c str>,
    Self: TryInto<String, Error = DatumConversionError<Self>>,
{
    /// Try to interpret datum as Unicode string
    fn try_as_str(&self) -> Result<&str, DatumViewError>;
}

Using TryInto here, because of what @kpreid said here:

And I plan to not use for<'d> &'d Self: TryInto<&'d str, Error = DatumViewError> either, due to the error I described here:

P.S.: In either case, there must be a compiler bug. Those hints aren't right!

I'd still suggest filing an issue for the diagnostic problems, if you can't find an existing one. I think there's at least two levels going on:

  • This is just a malformed suggestion
    &'a str: for<'a> From<&'a T>
    
  • It's finding some chain of blanket implementations and trying to get you to satisfy the deepest (?) one of those, when you would be better off (more general) specifying the most shallow constraint needed. This is what your "I didn't even use into()" note relates to -- if you implement From, you get Into, so you get TryFrom, so you get TryInto. But the blanket implementations are not the only ones! Perhaps they are the only ones that the compiler has not ruled out at the point of diagnostics though. (Wild guess on my part.)
    • I can supply a related but simpler example around ToOwned and Clone if useful.

If it's not much work to share? I'm happy to hear about it. Maybe it helps me to understand better what's exactly failing.

Even if I can work around these issues, I would like to know what is/was going on, and I'd like to provide feedback to the compiler developers. Though I also have to admit that once the type signatures get too complicated, it's difficult for me to understand what's happening. :hot_face:

Meanwhile I deal with four lifetimes in my code: 'a as a lifetime argument which helps me to accept closures that aren't static (but live as long as my virtual machine), 'b as a lifetime argument that indicates how long the virtual machine is living at least (which I need for internal references), 'c as a lifetime argument when I create temporary datums from borrowed values (to avoid extra memcpy), and 'd for HRTBs, hence I end up with stuff like:

for<'c, 'd> &'d <M as Machine<'a>>::Datum<'b, 'c>: TryInto<&'d str, Error = MachineError>

And this doesn't even cover the case yet, where MachineError is replaced by a generic type, as discussed in: Problem with HRTBs and associated type bounds.

So the overall complexity (in addition to the compiler errors I encountered) makes it difficult sometimes to maintain an overview…

This code:

fn foo<K, F>(f: F)
where
    F: Fn(&String) -> Cow<'_, K>,
    for<'any> Cow<'any, K>: PartialEq,
    K: /* ToOwned + */ ?Sized,
{
    // ...
}

fn main() {
    foo(|s| Cow::<'_, str>::Borrowed(&s[0..1]));
}

Elicits this suggestion:

5   |     F: Fn(&String) -> Cow<'_, K>,
    |                       ^^^^^^^^^^ the trait `Clone` is not implemented for `K`
    |
    = note: required because of the requirements on the impl of `ToOwned` for `K`
note: required by a bound in `Cow`
help: consider further restricting this bound
    |
7   |     K: /* ToOwned + */ ?Sized + std::clone::Clone,
    |                               +++++++++++++++++++

Due to this blanket implementation. And it's true, applying that suggestion would make foo itself compile. However, the program as a whole will still fail, as str does not implement Clone -- and nor do the other types that have implementations in the standard library. (And perhaps some downstream types, too.) That's why they have their own implementations after all!

The more general suggestion, and the bound desired here, is ToOwned (as is commented out).

I tried to create a smaller example, which exhibits the same behavior:

use std::borrow::Cow;

fn foo<K, F>(f: F)
where
    F: Fn(&String) -> Cow<'_, K>,
    K: /* ToOwned + */ ?Sized,
{
}

fn main() {
    foo(|s| Cow::Borrowed("X"));
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `K: Clone` is not satisfied
   --> src/main.rs:5:23
    |
5   |     F: Fn(&String) -> Cow<'_, K>,
    |                       ^^^^^^^^^^ the trait `Clone` is not implemented for `K`
    |
    = note: required because of the requirements on the impl of `ToOwned` for `K`
note: required by a bound in `Cow`
help: consider further restricting this bound
    |
6   |     K: /* ToOwned + */ ?Sized + std::clone::Clone,
    |                               +++++++++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to previous error

I meanwhile decided to do just that. I.e. the fallible conversion to &str just just uses a trait method now and won't support TryFrom/TryInto at all. Note that all remaining bounds are on Self (i.e. supertraits), which hopefully keeps most compiler bugs away! When converting owned values, I need to use .try_into() instead of String::try_from(…)) though.

This results in the following use in regard to fallible conversion from &Datum to &str:

use std::rc::Rc;
use std::cell::RefCell;
use my_sandboxing_system::prelude::*;
use my_sandboxing_system::{Callback, Compile, Machine};

fn collect_output<'a, 'b, M, C>(
    machine: &'b M,
    setup: C,
    run: C,
) -> Result<String, MachineError>
where
    M: Machine<'a> + Compile<'a, C> + Callback<'a>,
    for<'c> <M as Machine<'a>>::Datum<'b, 'c>: MaybeString<'c>,
{
    let output_cell = RefCell::new(String::new());
    let output_rc = Rc::new(output_cell);
    let output_weak = Rc::downgrade(&output_rc);

    let my_print = machine
        .callback_1arg(move |s| {
            output_weak
                .upgrade()
                .ok_or("closure expired")?
                .borrow_mut()
                .push_str(s.try_as_str()?);
            Ok([])
        })?;

    machine.compile(None, setup)?.call([my_print])?;
    machine.compile(None, run)?.call([])?;

    Ok(Rc::try_unwrap(output_rc).unwrap().into_inner())
}

By the way, the .call([…]) syntax with variable arguments has been realized through:

    fn call<'c, A>(&self, args: A) -> Result<Vec<Self::Datum<'static>>, MachineError>
    where
        A: IntoIterator<Item = Self::Datum<'c>>,
        <A as IntoIterator>::IntoIter: ExactSizeIterator;

This allows passing either a Vec<T> or an [T; _], as I don't really depend on having a Vec available (but only need .len() and being able to iterate over the values). Not sure if that's idiomatic, but I like the shortened syntax when passing a variable number of arguments.

P.S.: Also note that removing main still causes a weird/wrong error. Ideally, I'd not want to have to restrict K at all, because K: ToOwned could be deduced by the compiler, but I guess that's just yet another consequence of #20671, but more subtle. Or am I wrong? (Disregarding the wrong hint regarding Clone , of course.)

It's a similar implied bounds issue. Maybe it's part of that issue although we're talking about a type here, not a trait; I'm not going to go review it right now. The implied bounds RFC would fix it (Cow was an example in the RFC even). Here's something more minimal just in terms of hints:

struct S<T: ToOwned>(T);
fn bar<T>(_: S<T>) {}

(On the surface this may seem less problematic due to being Sized, but really, that's incidental.)

The main in my example was to illustrate that the suggestion is inadequate (it's an error after applying the suggestion, but not an error if the bound from the type declaration is copied over).

When you say

Ideally, I'd not want to have to restrict K at all, because K: ToOwned could be deduced by the compiler,

Well, that's what the implied bounds RFC is about.

Yes, but I think there's a difference between

  • deducing those bounds automatically and allowing them to be used (e.g. in a function's body)

and

  • requiring them to be added by the programmer, even if the programmer doesn't use any of those bounds in the function body, like you showed here:

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `T: Clone` is not satisfied
 --> src/lib.rs:2:14
  |
2 | fn bar<T>(_: S<T>) {}
  |              ^^^^ the trait `Clone` is not implemented for `T`
  |
  = note: required because of the requirements on the impl of `ToOwned` for `T`
note: required by a bound in `S`
 --> src/lib.rs:1:13
  |
1 | struct S<T: ToOwned>(T);
  |             ^^^^^^^ required by this bound in `S`
help: consider restricting type parameter `T`
  |
2 | fn bar<T: std::clone::Clone>(_: S<T>) {}
  |         +++++++++++++++++++

For more information about this error, try `rustc --explain E0277`.
error: could not compile `playground` due to previous error

You can't know if I'm using them or not without doing a full-program analysis or the like in the general case.

fn bar<T>(s: S<T>) {
    s.do_something(); // Am I making use of ToOwned? Maybe!
}

But I'm not really sure why you're drawing that distinction now anyway, when you just said you wanted the compiler to infer the bound (not ignore it if you don't use it).


Anyway, that's all another topic that need not be hashed out. My point is, the diagnostic is bad.

If you do, you could be required to add the bound in .do_something()'s signature, right? (And then you'd be required to also add it to bar's signature.

Of course I want to have everything!! I just thought maybe there's a quicker partial solution that doesn't require so much work to be done for the compiler developers.

Agreed.


P.S.: Should I write an issue regarding this problem in post #4 of this thread, or would you write something? Does this belong to #20671, or is it a separate issue?

I realized, maybe what I actually want/need is something like TryAsRef (which doesn't exist). Perhaps I'll make such a trait for my usecase and ensure it's turbofishable, so I can write .try_as_ref::<str>() instead of .try_as_str(). Anyway, it all feels like patchwork.


And for copyable types, I'd need to use something else. And it also would need to be turbofishable, so I'd need to use tricks like TypeIsEqual, which @steffahn uses in the into_ext crate (found it through this post). :+1:

But I'm not really sure I want to use all these tricks and workarounds. I wonder if perhaps the simpler solution to just use ordinary trait methods for try_as_… (like try_as_str above) is the solution with less headache in the end. But I'm not sure. I also expect some limitations of Rust to be lifted in future (hopefully). But not sure if From/Into and TryFrom/TryInto will really work well on references in the end (as there is AsRef for that purpose).

IMO this deserves it's own bug

&'a str: for<'a> From<&'a T>

The chain of blanket impls could piggy-back on #87272.

I might get around to the latter (but don't let that stop you from doing so first). I probably won't get around to the former.

https://github.com/rust-lang/rust/issues/87272#issuecomment-1009751027

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.