Generic impl TryFrom impossible due to orphan rules – what should I do?

I'm currently working on a crate to allow sandboxing different interpreted languages in Rust (e.g. running Lua in Rust, but also other languages).

The different machines, which will support different lanuages, implement the following trait (simplified version):

pub trait VirtualMachine {
    type Datum: BasicDatum;
    fn run(
        &mut self,
        code: &str,
        args: Vec<Self::Datum>,
    ) -> Result<Vec<Self::Datum>, Box<dyn std::error::Error>>;
}

As each language has their own (dynamic) type system, I use an associated type VirtualMachine::Datum to denote what kind of arguments and return values the language operates with.

To later be able to switch languages, each Datum implements a minimal interface, given as trait BasicDatum (again simplified for the purpose of demonstrating the problem, i.e. only covering bools and integers):

pub trait BasicDatum {
    // extracts an i32 if value can be seen as i32
    fn i32(&self) -> Option<i32>;
    // extracts a bool if value can be seen as bool
    fn bool(&self) -> Option<bool> {
        self.i32().map(|x| x != 0)
    }
    // creates a datum (BasicDatum) from a given i32
    fn from_i32(value: i32) -> Self
    where
        Self: Sized;
    // creates a datum (BasicDatum) from a given bool
    fn from_bool(value: bool) -> Self
    where
        Self: Sized,
    {
        match value {
            false => Self::from_i32(0),
            true => Self::from_i32(1),
        }
    }
}

The above example does not utilize the TryFrom and TryInto traits. However, I would like to provide such implementations in order to be able to write datum.try_into::<i32>(), for example.

If I have a particular BasicDatum, let's call it SomeDatum, I can easily implement it as follows:

impl TryFrom<SomeDatum> for i32 {
    type Error = ();
    fn try_from(value: SomeDatum) -> Result<Self, Self::Error> {
        value.i32().ok_or(())
    }
}

As I will soon have more data types and more conversions into Rust types, I would like to avoid the boilerplate for each BasicDatum type.

But I can't do a generic implementation:

impl<T: BasicDatum> TryFrom<T> for i32 {
    type Error = ();
    fn try_from(value: T) -> Result<Self, Self::Error> {
        value.i32().ok_or(())
    }
}

I get the following errors:

error[E0119]: conflicting implementations of trait `std::convert::TryFrom<_>` for type `i32`
  --> src/main.rs:66:1
   |
66 | impl<T: BasicDatum> TryFrom<T> for i32 {
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: conflicting implementation in crate `core`:
           - impl<T, U> TryFrom<U> for T
             where U: Into<T>;

error[E0210]: type parameter `T` must be used as the type parameter for some local type (e.g., `MyStruct<T>`)
  --> src/main.rs:66:6
   |
66 | impl<T: BasicDatum> TryFrom<T> for i32 {
   |      ^ type parameter `T` must be used as the type parameter for some local type
   |
   = note: implementing a foreign trait is only possible if at least one of the types for which it is implemented is local
   = note: only traits defined in the current crate can be implemented for a type parameter

Some errors have detailed explanations: E0119, E0210.
For more information about an error, try `rustc --explain E0119`.

For a full example, see Playground.

Regarding the first error (E0119), I believe this is because there could be an infallible conversion. That error might (in theory) be avoided with a negative implementations, such as:

impl<T: BasicDatum> !From<T> for i32 {}

But this won't work for the same reason why I can't fix the second error, which are orphan rules (if I understand it right).

But what can I do? Should I…

  • write a macro that allows providing all TryFrom implementations for a new BasicDatum type? (And how do I make sure every BasicDatum type is doing so?)
  • not implement TryFrom / TryInto at all?
  • use an entirely different approach?

This seems like it might be the best approach. You could enforce the implementation (or at least enforce a TryInto implementation) with a : TryInto<i32> supertrait on BasicDatum.

I don't like it, but I was worried it might be the only way to get these implementations "for free".

I noticed that what I actually want would be a fallible conversion from &T (and not T) to i32 (where T: BasicDatum). In that case, enforcing an implementation seems difficult:

pub trait BasicDatum
where
    &'_ Self: TryInto<i32>,

Which lifetime to specify there?

If I could fix that somehow, then maybe I could implement BasicDatum::i32 in terms of self.try_into().ok():

pub trait BasicDatum
where
    &'_ Self: TryInto<i32>
{
    // extracts an i32 if value can be seen as i32
    fn i32(&self) -> Option<i32> {
        self.try_into().ok()
    }
    /* … */
}

Moreover, if I moved the method BasicDatum::i32 into an extension trait, I might even be able to enforce that it's the same implementation.

side node

But if the actual implementation is in the try_from methods, then it would mean I can't work with default implementations anymore. Mehhh :sob:

But I guess it's generally impossible to give a guarantee that a &T where T: SomeTrait always implements TryInto<SomeType> when SomeType isn't a local type (but, for example, i32). Is that right, or am I just missing the right syntax? If there exists no such syntax, isn't that be a significant limitation of these traits (TryFrom and TryInto)? Or maybe my use case is just exotic… :thinking:

So perhaps I should just give up on trying to use TryFrom and TryInto?

I guess this might be okay for you? (It uses a number of advanced trait trickery...)

use std::convert::TryInto;

// explanation of this trait: https://docs.rs/into_ext/0.1.2/into_ext/trait.TypeIsEqual.html
pub trait TypeIsEqual {
    type To: ?Sized;
}
impl<T: ?Sized> TypeIsEqual for T {
    type To = Self;
}

pub trait RefTryInto<T>: for<'a> RefTryIntoOf<'a, T> {}

pub trait RefTryIntoOf<'a, T, _RefSelf = &'a Self> {
    type _RefSelf: TypeIsEqual<To = _RefSelf> + TryInto<T>;
}

impl<'a, S: ?Sized, T> RefTryIntoOf<'a, T> for S
where
    &'a S: TryInto<T>,
{
    type _RefSelf = &'a S;
}

impl<S: ?Sized, T> RefTryInto<T> for S where for<'a> &'a S: TryInto<T> {}

type RefTryIntoError<'a, S, T> = <<S as RefTryIntoOf<'a, T>>::_RefSelf as TryInto<T>>::Error;

fn ref_try_into<T, S: RefTryInto<T> + ?Sized>(s: &S) -> Result<T, RefTryIntoError<'_, S, T>> {
    fn convert<T>(t: <T as TypeIsEqual>::To) -> T {
        t
    }
    convert::<<S as RefTryIntoOf<'_, T>>::_RefSelf>(s).try_into()
}

pub trait BasicDatum: RefTryInto<i32> {
    // extracts an i32 if value can be seen as i32
    fn i32(&self) -> Option<i32> {
        ref_try_into(self).ok()
    }
    // extracts a bool if value can be seen as bool
    fn bool(&self) -> Option<bool> {
        self.i32().map(|x| x != 0)
    }
    // creates a datum (BasicDatum) from a given i32
    fn from_i32(value: i32) -> Self
    where
        Self: Sized;
    // creates a datum (BasicDatum) from a given bool
    fn from_bool(value: bool) -> Self
    where
        Self: Sized,
    {
        match value {
            false => Self::from_i32(0),
            true => Self::from_i32(1),
        }
    }
}

Yes, the assumption then would be that a user of the BasicDatum trait would have to implement the TryFrom or TryInto themself, but then they wouldn’t need to implement fn i32 anymore.

1 Like

I guess you could go further and just make

trait BasicDatum: From<i32> + From<bool> + RefTryInto<i32> + RefTryInto<bool> {}

with a corresponding generic implementation. Then offer a macro to create T: From<bool> and bool: TryFrom<&bool> implementations based on the corresponding T: From<i32> and bool: TryFrom<&i32> implementations, and a user of your trait would only need to implement T: From<i32> and bool: TryFrom<&i32> as well as invoke the macro.

You could probably even spare the whole fancy trait trickery above and just make a straightforward

impl<T> BasicDatum for T
where
    i32: Into<T>,
    bool: Into<T>,
    for<'a> &'a T: TryInto<i32>,
    for<'a> &'a T: TryInto<bool>,

To enforce consistency, you could seal the BasicDatum trait, so that the generic impl is the only implementation.

2 Likes

Here's an idea for how to avoid the problem entirely: Make BasicDatum an enum (which your crate defines) instead of a trait. Then, make the bound in VirtualMachine be type Datum: From<BasicDatum>.

This means more copying (unless BasicDatum has a lifetime in std::borrow::Cow-like fashion) and is less ergonomic, but it means you don't need to provide-or-require many expected conversions to the Datum type, only require one.

:flushed:

I can understand each single language construct, but it's very difficult (for me) to comprehend your example as a whole. Maybe I'll spend some hours trying to understand it, but…

… luckily I read that far before my head exploded :hot_face:. However, I might still have to go through your first example (to get default implementations for certain methods).

For now, I tried your second proposal and modified it a bit. I added a marker trait (MakeBasicDatum) which needs to be implement such that implementing From/TryFrom for some types doesnt (accidentally) turn a type into a BasicDatum.

This is what I ended up with:

// Trait for different virtual machines
pub trait VirtualMachine {
    // contains values of different types
    // (for languages with dynamic types)
    type Datum: BasicDatum;
    // runs interpreter (irrelevant for the problem here)
    fn run(
        &mut self,
        code: &str,
        args: Vec<Self::Datum>,
    ) -> Result<Vec<Self::Datum>, Box<dyn std::error::Error>>;
}

// Minimal interface for `VirtualMachine::Datum`
pub trait BasicDatum: private::BasicDatumSealed {
    // extracts an i32 if value can be seen as i32
    fn i32(&self) -> Option<i32>;
    // extracts a bool if value can be seen as bool
    fn bool(&self) -> Option<bool>;
    // creates a datum (BasicDatum) from a given i32
    fn from_i32(value: i32) -> Self;
    // creates a datum (BasicDatum) from a given bool
    fn from_bool(value: bool) -> Self;
}

// Implement this marker trait and several `From`/`TryFrom` traits
// (see generic implementation below) to make a type a `BasicDatum`
pub trait MakeBasicDatum {}

// Private module for sealing
mod private {
    pub trait BasicDatumSealed {}
    impl<T> BasicDatumSealed for T
    where
        T: super::MakeBasicDatum,
        T: From<i32>,
        T: From<bool>,
        for<'a> i32: TryFrom<&'a T>,
        for<'a> bool: TryFrom<&'a T>,
    {
    }
}

// Generic implementation of BasicDatum
impl<T> BasicDatum for T
where
    T: MakeBasicDatum,
    T: From<i32>,
    T: From<bool>,
    for<'a> i32: TryFrom<&'a T>,
    for<'a> bool: TryFrom<&'a T>,
{
    fn i32(&self) -> Option<i32> {
        self.try_into().ok()
    }
    fn bool(&self) -> Option<bool> {
        self.try_into().ok()
    }
    fn from_i32(value: i32) -> Self {
        value.into()
    }
    fn from_bool(value: bool) -> Self {
        value.into()
    }
}

// Example machine
struct SomeMachine {}

// Example datum
struct SomeDatum {
    inner: i32,
}

// Implementation of `BasicDatum` for `SomeDatum` as follows:
impl MakeBasicDatum for SomeDatum {}

impl From<i32> for SomeDatum {
    fn from(value: i32) -> Self {
        SomeDatum { inner: value }
    }
}

impl From<bool> for SomeDatum {
    fn from(value: bool) -> Self {
        SomeDatum {
            inner: match value {
                false => 0,
                true => 0,
            },
        }
    }
}

impl<'a> TryFrom<&'a SomeDatum> for i32 {
    type Error = ();
    fn try_from(value: &'a SomeDatum) -> Result<Self, Self::Error> {
        Ok(value.inner)
    }
}

impl<'a> TryFrom<&'a SomeDatum> for bool {
    type Error = ();
    fn try_from(value: &'a SomeDatum) -> Result<Self, Self::Error> {
        Ok(value.inner != 0)
    }
}
// End of implementation of `BasicDatum` for `SomeDatum`

// The following code is irrelevant for the problem,
// but included to provide context for the use case.

impl VirtualMachine for SomeMachine {
    type Datum = SomeDatum;
    fn run(
        &mut self,
        code: &str,
        _args: Vec<Self::Datum>,
    ) -> Result<Vec<Self::Datum>, Box<dyn std::error::Error>> {
        match code {
            "print('Hello World!')" => println!("Hello World!"),
            _ => Err("Not implemented!")?,
        }
        Ok(vec![])
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut machine = SomeMachine {};
    machine.run("print('Hello World!')", vec![])?;
    Ok(())
}

(Playground)

This is surprisingly simple, compared to other approaches, and it seems to work.

However, there are some things that bug me:

  • I have to write the list of required conversions twice (once in the trait used for sealing and once in the generic implementation). Though this only happens one time and I don't have to do that for each concrete BasicDatum type, so it's not really a big issue.
  • Implementing a bunch of Froms / TryFroms is more wordy than implementing a single trait. It also is difficult to notice these implementations as something related. I feel a bit like I'm abusing the language here.
  • Is the sealed trait pattern really okay to use? It does feel to me like a dirty trick too, but I guess it's commonly used and accepted?
  • I would need to resort to macros to create default implementations for some conversions (e.g. from i16 when I already have one for i32). This seems to make implementation more noisy (for each type implementing BasicDatum).

Considering all this, I believe that doing it the other way around (i.e. expecting to implement a trait with some default implementations and creating From/TryFrom automatically) would create a nicer interface.

If I understand this right, doing it the other way around isn't possible without this "advanced trait trickery"? :fearful:

In either case, thanks for your input so far. I have several options to go now, already. I still wonder what's the most idiomatic approach. I feel like either option isn't optimal.

I'm not sure about the implications. It would mean I have two representations of a datum:

  • a "native" one from the machine, and
  • a "universal" one from the crate.

It forces me to conceive a universal data structure that will handle all future cases of machines and their (potentially) weird types. Some machines have Unicode strings, some have strings with an explicit encoding, others only have byte strings, some have floats and integers, others only one numeric type, etc.

In some cases, I might still want to use the features of a "native" type of a machine. This requires me to use .into() (or .try_into()) twice when I actually want to convert to a Rust type: one time for converting into the universal datum, and once more to convert it into an i32, for example. (Unless I misunderstand your proposal.)

In an earlier approach, I had a single Datum type for all machines (and not even an associated type), but I had to give that up due to the heterogeneity of interpreted languages and their types.

No, only the ones that you want to be able to provide generically to any machine. The VM would still accept its Datum, but BasicDatum would be convertible to the specific Datum.

Yes, but even if the interface for BasicDatum is final, new machines might require different storage to provide an efficient implementation. For example, when the VM only uses byte strings, it might be necessary to perform a check whether a byte string can be converted to a Unicode string. The result of such a check might need to be stored in the data structure as soon as someone tried to convert the datum into a &str.

I will need to think beforehand what sort of storage a particular VM might need to efficiently implement the interface. But perhaps there aren't soooo many cases I need to think of. Still could run into trouble if I forget something, I guess. (And it will require extra storage even when never needed.)

That’s normal when implementing sealed traits.

Naturally, you’d explain in the docs of the BasicDatum crate how it can be implemented.

Yeah, it’s reasonably common.

The “advanced trickery” doesn’t achieve the other way around either. It’s mostly a demonstration of a way to specify a supertrait-bound that’s not actually a bound on Self. It still assumnes that the TryFrom/TryInto implementation is manual and that the fn i32 is (default-)implemented based on it; not the other way around.

I do agree that the interface could be nicer. I feel like eventually there’s a need for a language feature to define trait-like templates that just abbreviates a way to implement a set of other traits. Without any need to resort to macros, but with a comparable effect; just easier to use since you invoke them with an ordinary trait impl syntax. These “trait-like templates” could perhaps even be actual traits, if there is the right infrastructure determining (or at least supporting) the necessary restrictions / conditions for trait implementations necessary to have an equivalent effect to “just a macro for a lot of boilerplate trait implementations with (essentially) the restrictions that orphan rules would place on those explicit implementations”.

Does that mean that in your first example (with TypeIsEqual, RefTryIntoOf, etc.), I can't invoke .try_into() in a generic function, such as:

fn print_datum_int<'a, D: BasicDatum>(value: &'a D) {
    let i: i32 = value.try_into().unwrap_or(0);
    println!("Value = {}", i);
}

Maybe I can combine both directions:

  1. Actual implementation of the conversions is done in a trait MakeBasicDatum.
  2. A macro generates all From and TryFrom implementations based on the functions in BasicDatumImpl.
  3. A generic implementation where T: BasicDatumImpl, T:From<i32>, … of the "to-be-used" trait BasicDatum for T will ensure that the implementations of From, TryFrom, Into, TryInto are the same as the implementations in the methods of BasicDatum.

I gave it a try:

// Trait for different virtual machines
pub trait VirtualMachine {
    // contains values of different types
    // (for languages with dynamic types)
    type Datum: BasicDatum;
    // runs interpreter (irrelevant for the problem here)
    fn run(
        &mut self,
        code: &str,
        args: Vec<Self::Datum>,
    ) -> Result<Vec<Self::Datum>, Box<dyn std::error::Error>>;
}

// Minimal interface for `VirtualMachine::Datum`
pub trait BasicDatum: private::BasicDatumSealed {
    // extracts an i32 if value can be seen as i32
    fn i32(&self) -> Option<i32>;
    // extracts a bool if value can be seen as bool
    fn bool(&self) -> Option<bool>;
    // creates a datum (BasicDatum) from a given i32
    fn from_i32(value: i32) -> Self;
    // creates a datum (BasicDatum) from a given bool
    fn from_bool(value: bool) -> Self;
}

// Implement this trait and use macro `make_BasicDatum!`
// to make a type a `BasicDatum`
pub trait MakeBasicDatum {
    fn i32_impl(&self) -> Option<i32>;
    fn bool_impl(&self) -> Option<bool>;
    fn from_i32_impl(value: i32) -> Self;
    fn from_bool_impl(value: bool) -> Self;
}

// Automatically generate `From`/`TryFrom` implementations
macro_rules! make_BasicDatum {
    ($type:ident) => {
        impl<'a> TryFrom<&'a $type> for i32 {
            type Error = ();
            fn try_from(value: &'a $type) -> Result<Self, Self::Error> {
                value.i32_impl().ok_or(())
            }
        }
        impl<'a> TryFrom<&'a $type> for bool {
            type Error = ();
            fn try_from(value: &'a $type) -> Result<Self, Self::Error> {
                value.bool_impl().ok_or(())
            }
        }
        impl From<i32> for $type {
            fn from(value: i32) -> Self {
                Self::from_i32_impl(value)
            }
        }
        impl From<bool> for $type {
            fn from(value: bool) -> Self {
                Self::from_bool_impl(value)
            }
        }
    };
}

// Private module for sealing
mod private {
    pub trait BasicDatumSealed {}
    impl<T> BasicDatumSealed for T
    where
        T: super::MakeBasicDatum,
        for<'a> i32: TryFrom<&'a T>,
        for<'a> bool: TryFrom<&'a T>,
        T: From<i32>,
        T: From<bool>,
    {
    }
}

// Generic implementation of BasicDatum
impl<T> BasicDatum for T
where
    T: MakeBasicDatum,
    for<'a> i32: TryFrom<&'a T>,
    for<'a> bool: TryFrom<&'a T>,
    T: From<i32>,
    T: From<bool>,
{
    fn i32(&self) -> Option<i32> {
        self.try_into().ok()
    }
    fn bool(&self) -> Option<bool> {
        self.try_into().ok()
    }
    fn from_i32(value: i32) -> Self {
        Self::from(value)
    }
    fn from_bool(value: bool) -> Self {
        Self::from(value)
    }
}

// Example machine
struct SomeMachine {}

// Example datum
struct SomeDatum {
    inner: i32,
}

impl MakeBasicDatum for SomeDatum {
    fn i32_impl(&self) -> Option<i32> {
        Some(self.inner)
    }
    fn bool_impl(&self) -> Option<bool> {
        Some(self.inner != 0)
    }
    fn from_i32_impl(value: i32) -> Self {
        SomeDatum { inner: value }
    }
    fn from_bool_impl(value: bool) -> Self {
        SomeDatum {
            inner: match value {
                false => 0,
                true => 0,
            },
        }
    }
}

make_BasicDatum! { SomeDatum }

// Some generic function
fn print_datum_int<'a, D: BasicDatum>(value: &'a D)
where
    i32: From<&'a D>, // can I somehow get rid of this bound?
{
    let i: i32 = value.try_into().unwrap_or(0);
    println!("Value = {}", i);
}

// The following code is irrelevant for the problem,
// but included to provide context for the use case.

impl VirtualMachine for SomeMachine {
    type Datum = SomeDatum;
    fn run(
        &mut self,
        code: &str,
        _args: Vec<Self::Datum>,
    ) -> Result<Vec<Self::Datum>, Box<dyn std::error::Error>> {
        match code {
            "print('Hello World!')" => println!("Hello World!"),
            _ => Err("Not implemented!")?,
        }
        Ok(vec![])
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut machine = SomeMachine {};
    machine.run("print('Hello World!')", vec![])?;
    Ok(())
}

(Playground)

This works. What do you think of my combined approach? But I still can't get rid of the bound here:

fn print_datum_int<'a, D: BasicDatum>(value: &'a D)
where
    i32: From<&'a D>, // can I somehow get rid of this bound?
{ /* … */ }

Is there any way to fix this?

And isn't my overall approach a bit too far-fetched? I use sealed traits, macro generated implementations, generic implementations, etc… I feel like I'm doing something wrong, or that I'm sucked into an endless spiral of complexity just for trying to achieve an implementation of From/TryFrom. I feel a bit like these traits are difficult to deal with (on the implementation side). I almost hope someone will tell me that it's semantically wrong to use them in my use-case, so I have an excuse to not implement all this.

Indeed, you couldn’t do that. Mostly because rustc isn’t “smart enough” to understand the construct with the associated _RefSelf type and the TypeIsEqual trait. Maybe it would even be accepted one day, who knows? Currently, you’d need to use the ref_try_into function or any equivalent function. The trick with the convert helper function is necessary to exploit the fact that &D is the same type as <S as RefTryIntoOf<'_, T>>::_RefSelf, the latter being a type for which the compiler is aware of a TryInto<i32> bound. Make sure to read my documentation of the TypeIsEqual trait as presented in the into_ext crate, which I linked.

Yes, this seems reasonably, too. You could also add default-implementations of the bool-functions of MakeBasicDatum. You could consider changing the naming and/or the fact that some methods use self in order to avoid conflicts with the methods of BasicDatum; or maybe it’s enough to document the recommendation, never to use …::MakeBasicDatum; (and also not to use any T: MakeBasicDatum trait bounds).

It depends. My “trickery” above should allow you to remove any such bounds from any functions you write yourself, with some effort spent on changing the function implementation. Of course, if the bound is necessary for a simple method-call, you could use the BasicDatum methods instead. Any more complex use case, e.g. of other functions with _: TryInto<_> bounds or similar, a helper function such as the convert function used in ref_try_into with a sufficiently complex type will – I believe – always be able to do the job, but that’s not great user experience, of course.

AFAIK, there’s plans to eventually allow other supertrait bounds that restrict other types like &Self, not just Self itself, removing the need for any of the workaround trickery I’ve used; IIRC, such changes might blocked on chalk being completed and integrated, so it’ll still take a while.

For example with my original code above as the base, you can call a function like

fn print_datum_int<'a, D: BasicDatum>(value: &'a D)
where
    &'a D: TryInto<i32>, // can I somehow get rid of this bound?
{ /* … */ }

from a function like

fn print_datum_int_no_extra_bound<'a, D: BasicDatum>(value: &'a D)
{ /* … */ }

Took me longer than expected, and I didn’t really use a helper function but a helper trait (it might not be possible with a function alone), but whatever, here we go convincing the compiler to transfer the TryInto<i32> bound from …::_RefSelf to &'a D:

fn print_datum_int_no_extra_bound<'a, D: BasicDatum>(value: &'a D) {
    enum Ty {}
    trait HelperTrait<'a, D, T> {
        fn call(value: &'a D)
        where
            T: TryInto<i32>;
    }
    impl<'a, T, D, S: OtherHelperTrait<'a, D, <T as TypeIsEqual>::To>> HelperTrait<'a, D, T> for S {
        fn call(value: &'a D)
        where
            T: TryInto<i32>,
        {
            <Self as OtherHelperTrait<'a, D, T>>::call(value)
        }
    }
    trait OtherHelperTrait<'a, D, T> {
        fn call(value: &'a D)
        where
            T: TryInto<i32>;
    }
    impl<'a, D: BasicDatum> OtherHelperTrait<'a, D, &'a D> for Ty {
        fn call(value: &'a D)
        where
            &'a D: TryInto<i32>,
        {
            print_datum_int(value)
        }
    }
    <Ty as HelperTrait<'a, D, D::_RefSelf>>::call(value)
}

(playground)

Regarding user experience… I mean… it might be possible to – somehow – create a macro that can do this to any function that wants to drop bounds like &'a D: TryInto<i32> on a type with a D: BasicDatum trait? I have no idea how straightforward that is though, and then how usable such a macro would be.

Yes, that's what I have had in mind when considering this approach.

Yeah, (but) that makes the whole effort a bit useless.

Complex type systems seem to be… well, complex. It's interesting how difficult things get when you have more than a few concepts. And adding things like functional dependencies, associated types, higher-ranked bounds, higher-kinded types, etc. will/would make it even more complex. But I also think that the type system is the essence of what makes a programming language capable to allow certain abstractions.

That said, I think even with the most basic concepts such as traits and impls, Rust already provides much more than many other languages in that matter.

Without really understanding your code (yet), it reminds me a bit of GenericArray. These things scare me :grin:. Maybe if you find a method to seal the dark magic away, they might be of use. I think I'll better not include these tricks directly in my code (unless all wrapped in a module).

Maybe Rust should consider adding a darkmagic keyword, in addition to unsafe. :rofl:

After all these considerations, I think I will simply not use From/Into and TryFrom/TryInto.

That said, let me try to convince myself: From and Into tell you that you can perform a conversion, but these traits say nothing about the semantics of that conversion. Well, actually the docs (see From) give some hint on the semantics due to the example of error handling:

Used to do value-to-value conversions while consuming the input value.

Not much on semantics here, but further below:

The From is also very useful when performing error handling. When constructing a function that is capable of failing, the return type will generally be of the form Result<T, E>. The From trait simplifies error handling by allowing a function to return a single error type that encapsulate multiple error types. See the “Examples” section and the book for more details.

So basically the only context I read here is that these "conversions" are used during error handling. But maybe I'm too nitpicking here.

Anyway, my BasicDatum::i32 method does have a meaning. It's not just "convert this value into an i32", but more like: "can we interpret this value from a virtual machine (which might be fuzzy on data types) as an i32", which is slightly different, especially as the method works on &self and isn't consuming the receiver.

So perhaps I should just forget about From and TryFrom for this use-case (both for technical compilications and semantical considerations). But maybe I'm not really objective here, due to lack of satisfying options.

Even if I would implement From and TryFrom, I couldn't use it with an easy syntax on a generic type that is known to be a BasicDatum.

1 Like

Wisdom!

2 Likes

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.