Feature Request: Generics that can detect Traits

#[rustc_on_unimplemented] is per trait, but you can annotate arbitrary traits with this attribute if you enable the feature gate it is behind. It is much simpler that your proposal IMO. No boilerplate code is necessary for it at all.

#[rustc_on_unimplemented(
    message = "`main` has invalid return type `{Self}`",
    label = "`main` can only return types that implement `{Termination}`"
)]
pub trait Termination {
    /// Is called to get the representation of the value as status code.
    /// This status code is returned to the operating system.
    fn report(self) -> i32;
}
2 Likes

Maybe the amount of boilerplate in the code with specialization could be reduced with a macro taking into account the order of each impl:


trait Print0 { fn print0(&self, name: &str); }

multi_impl! {
    impl<T: Display> Print0 for T {
        fn print0(&self, name: &str) { println!("{}: (display) {}", name, self); }
    }
    impl<T: Debug> Print0 for T {
        fn print0(&self, name: &str) { println!("{}: (debug) {:?}", name, self); }
    }
    impl<T> Print0 for T {
        default fn print0(&self, name: &str) { println!("{}: not printable", name); }
    }
}

fn print_triple<A, B, C>(x : &(A, B, C)) {
    x.0.print0("A");
    x.1.print0("B");
    x.2.print0("C");
}

The macro would expand the code into this:

You can already do something extremely similar to this on nightly (requires the old, unsafe specialization feature):

#![feature(specialization, unsize)]

use std::marker::Unsize;
use std::fmt::{Debug,Display};

trait TryUnsize<T:?Sized> {
    fn try_unsize_ref(&self)->Option<&T>;
    fn try_unsize_mut(&mut self)->Option<&mut T>;
    fn try_unsize_box(self:Box<Self>)->Option<Box<T>>;
}

impl<T:?Sized, U:?Sized> TryUnsize<T> for U {
    default fn try_unsize_ref(&self)->Option<&T> { None }
    default fn try_unsize_mut(&mut self)->Option<&mut T> { None }
    default fn try_unsize_box(self:Box<Self>)->Option<Box<T>> { None }
}

impl<T:?Sized, U:?Sized> TryUnsize<T> for U where U:Unsize<T> {
    fn try_unsize_ref(&self)->Option<&T> { Some(self) }
    fn try_unsize_mut(&mut self)->Option<&mut T> { Some(self) }
    fn try_unsize_box(self:Box<Self>)->Option<Box<T>> { Some(self) }
}

fn print_triple<A, B, C>(x : &(A, B, C)) {
    print0(&x.0, "A");
    print0(&x.1, "B");
    print0(&x.2, "C");
}

fn print0<T>(x : &T, name: &str) {
    print!("{}: ", name);
    if let Some(x) = TryUnsize::<dyn Display>::try_unsize_ref(x) {
        println!("{}", x);
    }
    else if let Some(x) = TryUnsize::<dyn Debug>::try_unsize_ref(x) {
       println!("{:?}", x);
    }
    else {
        println!("not printable.");
    }
}

Playground

2 Likes

(tl:dr: fn (&T) -> Option<&dyn Display>)

That's a neat trick, though it won't work for non-dyn-compatible traits.

As written there are two problems. One, Rust doesn't have a way to pass traits as generic parameters. You could use dyn Trait as a stand-in for the trait itself, but then you have the same dyn-compatible limitation. Two, just knowing whether it impls the trait isn't good enough. You usually want to proceed to use methods from that trait; e.g. in the original example, println!("{} was operated on.", x); is implicitly calling x.fmt(…) from the Display trait. Even if does_impl::<Display>(x) returns true, that won't convince the compiler to let you call Display methods on x.

However, if we did have traits as generic parameters, you could perhaps have a function like:

fn try_cast<trait T, V>(v: &V) -> Option<&impl T>

which returns either Some(v) if V: T, or None otherwise.

In fact, I just realized you can already implement that on top of specialization for a specific trait (playground link):

trait TryCastDisplay {
    type D: Display;
    fn try_cast_display(&self) -> Option<&Self::D>;
}
impl<T> TryCastDisplay for T {
    default type D = &'static str; // dummy type that impls Display
    default fn try_cast_display(&self) -> Option<&Self::D> { None }
}
impl<T: Display> TryCastDisplay for T {
    type D = T;
    fn try_cast_display(&self) -> Option<&Self::D> { Some(self) }
}

fn foo<T>(x: &T) {
    match x.try_cast_display() {
        Some(d) => println!("{}", d),
        None => println!("not Display"),
    }
}

And I suppose you could have a macro that defines a "try cast" trait for a specified trait.

2 Likes

Cool, though once ! is stabilized we could use that as the default type.

Thanks for this example, that is actually basically what I had in mind.

1 Like

(A) Generalizing functions to work in other domains (polymorphism is a special case of this) and (B) defining a function to execute slightly differently if a trait is available are 2 very different things. (A) should be implemented by defining alternative functions, (B) should be defined within a function.

Implementing (A) using selective monomorphization is an antipattern because it requires modifying the original definition to expand the domain, which violates the open close principle.

Implementing (B) using specialization is also an antipattern because is misleadingly splitting up logic that should be in the same locality. It also violates the "just say what you mean" rule. Don't make people read code like that.

That's fine though. It just means that std::mem::needs_drop is effectively already selective monomorphization, so there would be no need to create any special case for it. If anything, it is a problem for specialization.

Sorry I wasn't specific enough about what I was referring to. I meant something more along the lines of:


fn do_complicated_thing<T>(x : &mut T) {
    do();
    some();
    stuff();
    if should have been impossible error condition occurs during debug {
        impl_check<T: Debug> {
            println!("More detailed error message: {:?}", x);
        }
        else {
            println!("Less detailed error message.");
        }
        panic!();
    }
    finish();
    the();
    calculation();
}

In real code that would be an assertion but I don't want to overcomplicate the example.

Imposing on a developer to write tons of boilerplate code in a far off location for something like this; it is badly dissuading developers from writing good code. In practice they'll only write the mediocre error message, or whatever the mediocre case is only if it is some other kind of check, because boilerplate is too much of a nightmare to write good code.

Also it forces the developer to use a fat pointer. Although it is handy that Rust has fat pointers as an option, there are languages like C# and Java that force developers to (almost) always hide data behind extraneous pointers with bloated header information, it is one of the vastly superior features of Rust to not impose that. It is part of the "zero cost abstraction" guarantee.

While this technically works, in practice I would expected it to be worse than just directly using specialization.Because when you need selective monomorphization is usually going to be in small isolated instances, not several times for the same trait, and this is even more boiler plate code than specialization would be.

Also, it isn't just 12 lines of boiler plate code for &T, it is another 12 lines for &mut T, and another 12 lines for Box<T>, and another 12 lines for [T; 7], etc. And how are you doing to define it for plain T without dropping the variable inside the block? Granted that's a rare use case, but I think there is some virtue in language completeness.

Also it has scope and maintenance problems like specialization, although in a different way. Which crate is going to define TryCastDisplayRef ? And TryDebugPrintVector? And how will we avoid having these defined again and again and again by every user that needs it? And I'm not sure, but can these explicitly be tagged as const? When I tried doing it on the playground it said "functions in traits cannot be const".

And there is just the plain problem that requiring 12 lines of faraway boilerplate code to do something as simple as an if/then/else concept is not good for developers. Java got to be hated for being such an obnoxiously verbose language like that.

Selective monorphization is local, doesn't have any crate maintenance issues to worry about. It is easy to use. It can provide very nice improvements in code. None of the "Unresolved questions" about specialization apply to it. And I would guess it would be much easier than specialization to implement and stabilize, but I have to defer to others on that.

Having disassembled similar code in the past (though never via the Unsize trait), I’m reasonably confident that the optimizer will inline and devirtualize this, so the result will actually be “zero cost.”

2 Likes

I'd really like to have this feature (basically a Rust version of static if / constexpr if) but I think it's going to be a hard sell.

If you really want to push it, if think the easiest way is to wait for specialization to be implemented, find use-cases in the wild that do what you want using specialization, and go "See? look how easier that code would be to read/write with a static if construct!", using some real-world examples.

5 Likes

The other way to gather those examples is to write a crate that (perhaps poorly) emulates the feature and show that it gets used in real-world projects. Then, you can show how some additional language features would improve the situation.

1 Like

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