Feature Request: Generics that can detect Traits

I would like to require a feature similar to specialization, but simpler and local. I expect this has probably been asked for before, by I couldn't find the keywords to find any posts.

It would be really nice to be able to ask a generic type if implements a trait without having to go through all of the trouble of making multiple definitions. My recommendation is:

    fn operate<T>(x: &T) {
        implcheck T:Display {
            println!("{} was operated on.", x);
        }
        else {
            println!("Input was operated on.");
        }
    }

During type checking, any T within an implcheck T:A+B+C {} block is assumed to implement A+B+C. Then during monomorphization, if the type T is implements the traits then the block is included, otherwise if the else block is present that is included, otherwise it is just skipped.

Although this could be done with specialization, it would be really hard on the developer. Specialization requires a ton of boiler plate. The dev has to make a new impl and a new function definition, for something that could be as easy as "hey, if this value can be printed, then print it".

Further, if there are x generics checking y traits on each of them, that requires an exponential number of boiler plate definitions: (y+1)^x. For example something like this:

    fn print_triple<A, B, C>(x : &(A, B, C)) {
        implcheck A:Display {
            println!("A: {}", A);
        }
        else implcheck A:Debug {
            println!("A: {:?}", A);
        }
        else {
            println!("A: not printable");
        }
        implcheck B:Display {
            println!("B: {}", B);
        }
        else implcheck B:Debug {
            println!("B: {:?}", B);
        }
        else {
            println!("B: not printable");
        }
        implcheck C:Display {
            println!("C: {}", C);
        }
        else implcheck C:Debug {
            println!("C: {:?}", C);
        }
        else {
            println!("C: not printable");
        }
    }

To do that with specialization, it would require 27 new definitions and impl blocks.

Also, implcheck is local to the function, it doesn't have to worry about all the problems of crates conflicting on definitions and such. I expect it would be a much easier safe zero cost abstraction to implement while specialization is still being worked out.

Keeping in mind that traits are static (i.e. non-runtime) constructs, I don't believe it makes much sense to to ask at runtime whether or not some type implements it.

So, the only alternative is to get an answer to that statically. If you want your code to only compile if some type implements some trait, then you can write a fn like this:

#[inline(always)]
#[allow(non_snake_case)] 
fn implements_MyTrait<T: MyTrait>(t: &T) -> bool { true } 

The compiler will refuse to compile the code if the argument passed to this fn doesn't implement MyTrait. If it does compile, there is no runtime overhead due to the inline attribute.

This should be resolved with monomorphization, no run time checks.

And what you wrote doesn't have an option for T : !MyTrait.

Indeed it does: refusal to compile. But yeah you won't get a boolean answer out of a !MyTrait argument using my suggestion.

I was thinking about downcasting, but that has the downside that you likely will have to write a fallible downcast method yourself. And yet again, that won't work for types that don't actually implement the trait with the downcast method.

So given that generic functions are monomorfized I would expect this to be some form of cfg annotation (or based on the cfg_if crate).

Because basically you want the versions of that function for the types that implement Trait to have some extra functionality.

Is that correct?

1 Like

The part about wanting functions to have extra (or less) functionality based on traits is correct.

The part about it being based on a crate I know nothing about. But I'm pretty sure it is impossible to do this with a crate, or someone would have mentioned it in all the things I've read.

I write a lot of custom container datatypes, that's what I need it for. Skipping drop code when the type isn't drop, or checking that the contained values are valid when such a trait would apply to them, or giving better error messages when a bug is detected. I don't need the hassle of specialization for any of that, just need to be able to check if a generic implements a trait.

This feature request will be solved with specialization.

std::mem::needs_drop

What does this mean?

This does exist on nightly, but not on stable. I don't remember what the attributes were atm

1 Like

It doesn't require an exponential number of impls. Each implcheck or else implcheck can be implemented as a trait definition plus two impls. For example, here is your example implemented with specialization. Quote:

trait Print0 { fn print0(&self, name: &str); }
impl<T: Display> Print0 for T {
    fn print0(&self, name: &str) { println!("{}: (display) {}", name, self); }
}
impl<T> Print0 for T {
    default fn print0(&self, name: &str) { self.print1(name) }
}
trait Print1 { fn print1(&self, name: &str); }
impl<T: Debug> Print1 for T {
    fn print1(&self, name: &str) { println!("{}: (debug) {:?}", name, self); }
}
impl<T> Print1 for T {
    default fn print1(&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");
}

Now, this is certainly verbose, and it would have required (1 trait definition + 2 impls) * 6 implchecks = 18 items, if I hadn't taken advantage of the fact that the logic is the same for A, B, and C. But it's not exponential: compared to your syntax there's only a linear multiple of the number of lines of code.

Also, most of the verbosity could be eliminated by a macro, although this would still require using separate fns per case and passing parameters to them as needed.

2 Likes

Over half my post was explaining how specialization is not a reasonable solution to this.

Is a half measure for a single trait that would be unnecessary if the problem was solved in generality.

Some types are valid in all states. usize is an example. Some types can store values which are not valid, a BTreeSet with fewer elements than it's count or a LinkedList that is circular are examples.

You could check if an instance of such a type is valid or not algorithmically, and define a trait Valid for such types that have that check defined. Furthermore, for something like BTreeSet<T>, you could you implcheck to check not only the correct structure of the container, but if T:Valid then you could check that the populated buffer locations also hold valid values.

If it does exist then it is another half measure, defined on a single trait. Rust should have a simple general solution for any user defined trait that doesn't require tons of boiler plate code to use.

As far as I can tell this is equivalent to the subset of specialization without specialized associated types, and where you can't introduce extra generic parameters on the impl (unless you extend this to support "implcheck for<U> T: MyTrait<U>").

Hence, it probably requires to stabilize specialization, and then it can be done with a macro on top of specialization.

1 Like

That is a good point, you could define a new function for each possible implcheck block, at which point the asymptotic complexity would be the same. But between this:

Or this:

fn print_triple<A, B, C>(x : &(A, B, C)) {
    x.0.print0("A");
    x.1.print0("B");
    x.2.print0("C");
}
fn print0<T>(x : &T, name: &str) {
    print!("{}: ", name);
    implcheck T:Display {
        println!("{}", x);
    }
    else implcheck T:Debug {
       println!("{:?}", x);
    }
    else {
        println!("not printable.");
    }
}

Honestly which one would you rather write, and which one would you rather read?

Also forcing a programmer to split up a function into multiple functions just because a language can't support such a feature is really bad practice.

Also, languages like C# can do this just fine:

        if (o is Employee e)
        {
            return Name.CompareTo(e.Name);
        }

Although C# cheats by having lots of extra wasted space as header information at run time and run time crashing casts. Rust could do a lot better.

That last is a runtime check similar to downcasting.

What I meant by using CFG is probably what yato means by building upon specialization. Namely you would have a const fn does_impl<T, V>(v: V) -> bool.

2 Likes

Oh that's also a good point, my suggested syntax would not take that in to account.

Oh yeah if that is possible in the future then maybe that would be enough. Could you explain in a little more detail how to this would be defined and used?


fn complicated_operation<T>(x : &T) {
    ....
    if does_impl::<Clone>(x) {
        /* how do I call x.clone() */
    }
    ...
}

I could see adding a closure, but that would mean you couldn't return from the block. Or maybe a macro but I don't know how the macro would do type checking.

If you were thinking of implcheck T: Drop for this, that's probably wrong! Types can need dropping without implementing Drop -- for example String only drops implicitly in its Vec<u8> field.

13 Likes

Huh. That's weird? Why does the language do that? Anyway the compiler has to know that String is Drop in order to call the drop operation, whether the language exposes it or not. Wouldn't that also be a problem for specialization though? You'd need a compiler warning or something for it. Does any other trait behave this way? I've seen Sized is weird, might also be an exception.

No, the compiler knows that String has a field that implements Drop, and it calls only that one.

There's also a warning for T: Drop that explains this in a little more detail: https://doc.rust-lang.org/rustc/lints/listing/warn-by-default.html#drop-bounds

3 Likes

Well the way that downcasting works is that it returns Option<T>.

But since I am imagining it as a lang item or something, it could just be scoped since you don't really want a new handle and wanting the conditional compilation to be guaranteed to be without a jump.

Re: downcasting, IIRC, although it's a runtime check, if you do it in a monomorphized function the runtime check gets optimized away.

Ah so a compiler intrinsic could do this. Something like:

fn checked_cast<Out, In>(src: In) -> Option<Out>

Where the language defines a reasonable set of rules?

1 Like

This has already been proposed and rejected, although I can't find the relevan link on GitHub. In general, a long if-else chain of type checks is an anti-pattern; if you want different types to behave differently, create your own trait and implement it for the pertinent types.

3 Likes