Specialization for better Debug for Any

Problem I’d like to solve:

catch_unwind returns an error as Box<Any>. Unless Any is a String or str or other predefined type I have to idea what is it. I cannot show good diagnostic message to the user (and to myself, and also I have to explicitly downcast it to print).

However, it is likely that real Any implements Debug. So what am I thinking: Debug of Any could delegate Debug impl to the real object if object actually implements Debug.

Thanks to specialization it is possible:

#![feature(specialization)]

use std::fmt;
use std::fmt::Debug;

trait MyAny {
    // merge this trait content into `Any`
    fn debug(&self) -> Option<&Debug>;
}

impl<T> MyAny for T {
    default fn debug(&self) -> Option<&Debug> {
        None
    }
}

impl<T : Debug> MyAny for T {
    fn debug(&self) -> Option<&Debug> {
        Some(self)
    }
}

impl Debug for MyAny {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self.debug() {
            None => fmt.pad("Any"),
            Some(debug) => write!(fmt, "{:?}", debug),
        }
    }
}




fn main() {
    let any1: Box<MyAny> = Box::new(true);
    let any2: Box<MyAny> = Box::new(vec![17, 19]);
    struct Foo {}; let any3: Box<MyAny> = Box::new(Foo {});

    println!("{:?}", any1);
    println!("{:?}", any2);
    println!("{:?}", any3);
}

prints:

true
[17, 19]
Any

Why not? (Except because minor library enhancements are not the highest priority now).

2 Likes

Instead of .debug(), is it possible to generalize .downcast_ref::<T>() so that T can be a trait?

@kennytm: The problem with that is that it requires RTTI. Rust doesn’t have RTTI. Any just doesn’t have the information to make that work. I’d expect any such feature to come along with (and probably depend on) thin object pointers.

Not sure what the relationship with thin object pointers is.

There’s a contraption I did two years ago and @Diggsey has a brand new independent implementation and the only requirement is listing all the traits you want to expose ahead of time.

Removing that requirement ends up with polymorphic and cross-crate concerns, besides needing compiler support.

If you only generate the RTTI information only in executable crates then the cross-crate concern more or less go away and you can get all the monomorphic trait impls.

Polymorphic trait RTTI requires a much more complex system that can reflect on structural types and polymorphic nominal types (i.e. the whole typesystem) and effectively do trait impl selection at runtime.

It’s doable (Haskell and Julia are relevant examples, I believe), and Rust could do it without garbage collection, but it would be a lot of work for a slow system (if you don’t also throw a JIT in, which is even more work).

I think being able to downcast to arbitrary traits would probably be a misfeature. There’s some discussion on this GitHub issue about why.

What is your use case for downcasting to Debug @stepancheg? Why are you using catch_unwind and then printing error messages for the user?

What is your use case for downcasting to Debug @stepancheg? Why are you using catch_unwind and then printing error messages for the user?

The server (e. g. HTTP server) catches panic in provided request handler and sends an error to the client (instead of simply crashing). Panic message can be formatted and sent over the network and shown to the client (e. g. in browser). Detailed message is often more helpful than generic message "handler panicked".

I think being able to downcast to arbitrary traits would probably be a misfeature.

While it's the case that there are sometimes better options, I don't think it's fair to call it a misfeature. The thread you linked to is mostly weighing up the benefits of parametricity, and yet rust has never had runtime parametricity, and downcasting does not break compile-time parametricity (which rust has also explicitly opted out of with specialization).

A case where this is useful is when a library needs to take ownership of some objects which it doesn't know the type of. If they're all the same type, then you can often use generics, but if they're heterogenous, you have to use Box/Rc with a trait object.

Now the user of your library has no control over what traits they expect these objects to implement: there's no way for the library to say "I need these objects to be cloneable", and for the user to say "I need these objects to implement Debug`, and to get a sensible trait object that meets those two requirements. In practice, the library author has to make a decision over what traits they're going to require, and the end user is on their own if they need anything else.

The minimum set of pieces that rust would need to solve this problem the "proper" way are:

  • HKT, so that the library's API allows the traits to be specified by the user
  • Multiple-trait trait objects, so that &(X + Y+ Z) is a valid trait object
  • Trait object up-casting, so the library can use &(X + Y + Z) as an &(X + Y)

Alternatively, you just support trait object downcasting, and now the library can specify its own bounds at compile time (eg. Any + Debug) and have that be statically verified, while the user of the library always has an "out", so they can access additional functionality on the object at runtime, if it exists.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.