[Solved] Using generic type parameter in expression?

Is there a way to use a generic type parameter in an expression?

For example:

fn SomeGenericFunc<T>() {
    if T : Display {
        // Here we know T implements Display
        println!("{}", T);
    } else if T : Debug {
        // Here we know T implements Debug
        println!("{:?}", T);
    } else {
        println!("Hey, don't know how to print!");
    }
}

Obviously, during actual code generation when instantiating the actual types, the irrelevant code branches can be safely omitted.

No, you can do something similar with specialization, altough doing this sort of checks with specialization won't be possible without the lattice rule. see @qnighy's playground below to see how to do this.

1 Like

Isn't this possible by applying two specializations, just like the ordinary else if semantically being just a nested pair of if-elses?

1 Like

See @qnighy's playground below for solution! My answer here is wrong.


Yes, but you can only specialize something like below without the lattice rule

if T: Foo {
    if T: Bar {
    } else {
    }
} else {
}

Because we don't have negative trait bounds.

Well, I thought code wins arguments in this case: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=14a75c52dbb0900d8daedb1e226aaef8

3 Likes

Incidentally, I find this kind of trickery very unidiomatic and ugly. It is akin to the "dynamic_cast in an if-else chain" anti-pattern in C++. It stuffs an entire chain of checks without much structure in a single function.

I was going to write that "a helper trait with explicit blanket impls would make this more structured", but then I realized that in the meantime @qnighy has already provided that solution. It's much neater, as it scales and refactors a lot better, even if initially a bit longer.

That's clever, I didn't think of using multiple spec traits!

It doesn't have to be ugly. It can be something like match:

match T {
    Display => ...,
    Debug => ...
    _ => ...
}

Obviously, in actual code generation, the entire match statement will go away and only one match will ever be instantiated for each type.

I agree that the proper way of doing this will probably be to create a zillion different small traits... but sometimes we are lazy...

True, but it is not the same. That usually requires creating a custom trait just to track what we're trying to do on an abstract level, and multiple impl blocks.

What if we only need to do this once somewhere? Or it is really localized, implementation-specific logic that somehow must distinguish among a few different types for fine-tuned treatment...

Using type parameter in expression is quick-n'-dirty... Of course, the benefits are quick, but at the cost of dirty...

In JavaScript you can use instance of. In C#, for example, you can do something like:

void MyGenericFunction<T>(T p) {
   if typeof(T) == typeof(int) {
        int t1 = (int) (object) p;
    .... do something with the int
    }
}

It cuts down on the amount of code. But it makes the code logic less clear by mingling types into code.

It's not the syntax that bothers me – it's the semantics of magically changing behavior based on a heterogeneous type. That basically erases all the benefits of static typing and parametric generics, resulting in the same kind of mental pressure in the reader that one encounters when working in a dynamically-typed language.

I'd argue that the real need for this kind of code is so extremely rare that it should not be a problem to write the aforementioned small extension traits once in a blue moon. (And even if we accept the laziness/inconvenience argument as valid from an ergonomics point of view, from a correctness point of view it's still better to stop and think thoroughly about the implications of the heterogeneous behavior of the resulting code, while hammering out the traits by hand.)

4 Likes

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