Is there a way to detect whether some generic is a Result regardless of the generics of the Result itself

I have a trait implementation on any T that implements serde::Serialize however I noticed that Result<T,E> is Serialize too as long as the generics are Serialize.
What I would like to do is to detect and panic if Self is any sort of Result.

trait MySuperCoolTrait{
  fn do_soemthing(&self)->Result<...>
}
impl <T> MySuperCoolTrait for T
where T:Serialize
{
  fn do_something(&self)->Result<...>
  {
    //Ideally this would work
    if TypeId::of::<Result>() == self.type_id()
    {
      panic!("you really should not do this")
    }
  }
}

There's no reliable way to do this currently. Using specialization, you could implement a trait for Result<T, E> and all other types separately, with something like an is_result(&self) -> bool method.

However, it seems like a very questionable idea, for multiple reasons:

  • You should really-really not panic in a function that peope expect to be fallible (handle errors gracefully) based on its return type. If you are returning a Result, you should return Err to signal errors instead of panicking.
  • If all you need is a serializeable type, and Result is serializeable, why would you actively prevent someone from using your function with that result?
  • It's futile. Someone who wants to serialize a Result wil still be able to wrap it in a newtype, delegate Serialize to the wrapped result, and pass the wrapper to your function. There is absolutely nothing you can do about this.

Given that the formatting of your code snippet above is highly non-idiomatic, I suspect that you are a beginner in Rust, perhaps coming from a traditional object-oriented language or something like that. What are you actually trying to achieve with such a restriction? If you state your high-level goal instead, we might be able to suggest a better, less surprising, more streamlined solution to your real requirement.

4 Likes

well this is not a library code so its not for public use and it is really okay to panic in my case because this should not be used with a result results should be early returned so error handler can catch and do the necessary thing and by panicking i would be able to see where i forgot to add a ? patch that

Ignoring the question of whether this is idiomatic or not, you could deliberately create a disambiguity regarding the method name by implementing (and importing, if used outside) a separate trait, which is only implemented for Results, providing the same method name. If you do not call the method with fully qualified syntax, you get a compiler error when the method name is ambiguous.


Example:

trait Serialize {}
impl Serialize for String {}
impl<O: Serialize, E: Serialize> Serialize for Result<O, E> {}

mod m {
    use super::Serialize;
    pub trait MySuperCoolTrait {
        fn do_something(&self) -> Result<(), ()> {
            Ok(())
        }
    }
    impl<T: Serialize> MySuperCoolTrait for T {}
    pub trait DoNotUse {
        fn do_something(&self) -> Result<(), ()> {
            unimplemented!()
        }
    }
    impl<O, E> DoNotUse for Result<O, E> {}
}

use m::MySuperCoolTrait;

#[allow(unused_imports)]
//use m::DoNotUse as _; // uncommenting this makes the last line in `main` fail to compile

fn main() {
    struct X;
    impl Serialize for X {}
    X.do_something().ok();
    Ok::<X, String>(X).do_something().ok();
}

(Playground)

3 Likes

this is pretty cool actually :slight_smile: this is more of a hack tho i just wanted to see if theres any proper way to do this with but apparently not

The "proper" way would be to use specialisation.

That said, maybe you can take a step back and avoid putting the if is_result() { panic!() } in your code entirely. If this isn't for a library or public use, I'm guessing you've got complete control over how the code is called, and if that's the case there's no reason why you can't manually call .unwrap().

Even @jbe's example won't work in a generic context because it relies on knowing the concrete type. This technique is a variation on "autoref specialisation", and dtolnay explains the limitations better than I ever could:

Limitations

The way that this technique applies method resolution cannot be described by a
trait bound, so for practical purposes you should think of this technique as
working in macros only.

That is, we can't do:

pub fn demo<T: ???>(value: T) -> String {
    (&value).my_to_string()
}

and get the specialized behavior. If we put T: Display in the trait bound,
method resolution will use the impl for T: Display even if T happened to be
instantiated as String.

Depending on your use case, this is honestly fine! If you are a macro already
then you're all set. If you can be made a macro, that's good too (like I did for
anyhow! (though it was good for that to be a macro anyway so that it can
accept format args the way println does)). If you can't possibly be a macro then
this won't help you.

I am excited to hear other people's experience applying this technique and I
expect it to generalize quite well.

2 Likes