Static dispatch - short and long syntax

Hi folks,

I played with different syntax for static dispatch and I realized that the second example for ReturnThat trait doesn't work:

trait TakeThis {
    fn what_is_it(&self) -> String;
}

impl TakeThis for u8 {
    fn what_is_it(&self) -> String {
        format!("u8:     {}", self)
    }
}

impl TakeThis for String {
    fn what_is_it(&self) -> String {
        format!("string: {}", self)
    }
}

struct Orange {
    name: String,
    color: (i32, i32, i32),
}
struct Tomato {
    name: String,
    size: String,
}

trait ReturnThat {
    fn describe_it(&self) -> String;
}

impl ReturnThat for Orange {
    fn describe_it(&self) -> String {
        format!("fruit: {} orange, color {}-{}-{}", self.name, self.color.0, self.color.1, self.color.2)
    }
}

impl ReturnThat for Tomato {
    fn describe_it(&self) -> String {
        format!("veg: {} tomato, size {}", self.name, self.size)
    }
}

/////////////////////////////////////////////////////////////////////////
// FIRST EXAMPLE - OK
// short syntax
fn take_static_dispatch_short_syntax<T: TakeThis>(x: &T) -> String {
    x.what_is_it()
}
// long syntax
fn take_static_dispatch_long_syntax<T>(x: &T) -> String
where
    T: TakeThis,
{
    x.what_is_it()
}
/////////////////////////////////////////////////////////////////////////
// SECOND EXAMPLE - DOES NOT WORK
// short syntax
fn return_static_dispatch_short_syntax(fruit: bool) -> impl ReturnThat {
    if fruit {
        Orange {
            name: "Great".to_owned(),
            color: (3, 4, 5),
        }
    } else {
        Tomato {
            name: "Super".to_owned(),
            size: "cherry".to_owned(),
        }
    }
}
// long syntax
fn return_static_dispatch_long_syntax<T>(fruit: bool) -> T
where
    T: ReturnThat,
{
    if fruit {
        Orange {
            name: "Great".to_owned(),
            color: (3, 4, 5),
        }
    } else {
        Tomato {
            name: "Super".to_owned(),
            size: "cherry".to_owned(),
        }
    }
}
/////////////////////////////////////////////////////////////////////////

fn main() {
    let x = 5u8;
    let y = "Hello".to_owned();

    println!("{}", take_static_dispatch_short_syntax(&x));
    println!("{}", take_static_dispatch_long_syntax(&x));
    println!("{}", take_static_dispatch_short_syntax(&y));
    println!("{}", take_static_dispatch_long_syntax(&y));
}

Could you please tell how to fix the second example?

Thank you.

I take it from the title that you don't want -> Box<dyn ReturnThat>?

-> impl Trait is an opaque alias around some other type. It doesn't let you return more than one type.

This doesn't work either:

fn return_static_dispatch_long_syntax<T>(fruit: bool) -> T
where
    T: ReturnThat,
{
    if fruit { Orange { .. } } else { Tomato { ... } }
}

The caller chooses what generics like T resolves to, not the function body. Inside the function body, T represents a single type that meets the bounds. And you still can't return more than one type. You have to return whatever T the caller chose.

If you want to return either a Tomato or an Orange without type erasure (dyn ReturnThat), you could use an enum.

#[non_exhaustive]
pub enum SomeReturnThat {
    Orange(Orange),
    Tomato(Tomato),
}

impl ReturnThat for SomeReturnThat {
    fn describe_it(&self) -> String {
        match self {
            Self::Orange(orange) => orange.describe_it(),
            Self::Tomato(tomato) => tomato.describe_it(),
        }
    }
}

And use that with -> SomeReturnThat or -> impl ReturnThat.

4 Likes

Thank you @quinedot

That's right.

Aha!

I was mainly trying to figure out a use case for returning T (use case for T as arg is obvious).

I managed to write the following. I think that's an example of "The caller chooses...not the function body."

#[derive(Debug)]
struct Orange {
    name: String,
    color: (i32, i32, i32),
}
#[derive(Debug)]
struct Tomato {
    name: String,
    size: String,
}

trait ReturnThat {
    fn describe_it(&self) -> String;
    fn clone_with_no_name(&self) -> Self;
}

impl ReturnThat for Orange {
    fn describe_it(&self) -> String {
        format!("fruit: {} orange, color {}-{}-{}", self.name, self.color.0, self.color.1, self.color.2)
    }
    fn clone_with_no_name(&self) -> Self {
        Self {
            name: "".to_owned(),
            color: self.color.clone()
        }
    }
}

impl ReturnThat for Tomato {
    fn describe_it(&self) -> String {
        format!("veg: {} tomato, size {}", self.name, self.size)
    }
    fn clone_with_no_name(&self) -> Self {
        Self {
            name: "".to_owned(),
            size: self.size.clone()
        }
    }
}

/////////////////////////////////////////////////////////////////////////
// short syntax
fn take_and_return_static_dispatch_short_syntax<T: ReturnThat>(fruit: T) -> impl ReturnThat {
    fruit.clone_with_no_name()
}
// long syntax
fn take_and_return_static_dispatch_long_syntax<T>(fruit: T) -> T
where
    T: ReturnThat,
{
    fruit.clone_with_no_name()
}
/////////////////////////////////////////////////////////////////////////

fn main() {
    let x = Orange { name: "tasty".to_owned(), color: (1,2,3)  } ;
    let y = Tomato { name: "super".to_owned(), size: "cherry".to_owned() } ;

    // println!("{:?}", take_and_return_static_dispatch_short_syntax(x)); // ERROR: DEBUG IS NOT IMPLEMENTED
    println!("{:?}", take_and_return_static_dispatch_long_syntax(x));
    // println!("{:?}", take_and_return_static_dispatch_short_syntax(y)); // ERROR: DEBUG IS NOT IMPLEMENTED
    println!("{:?}", take_and_return_static_dispatch_long_syntax(y));
}

I understand the error. What is surprising to me is that using generic T and impl ReturnThat are two different things - in other words it is not just a different syntax.

@quinedot's answer is very good, but I would like to extend it a little bit.

This is called "enum dispatch", and there is a popular crate that abstracts a lot of boilerplate.

Actually returning impl ReturnThat might still be a good idea even in this case. It all depends on what semver guarantees do you want to give to your caller. When public function returns concrete type, you cannot change it without major release. In above code snippet SomeReturnThat enum was marked as #[non_exhaustive], which gives you some flexibility (you can add new variants), but you cannot ever remove variants or change it to a dynamic dispatch. Type erasure allows it. And with recently stabilised associated type bounds feature, expressing complex bounds is very easy.

3 Likes

Unfortunately the meanting of impl Trait is context-specific. In almost all contexts where it's supported or planned to be supported, it has the "opaque type alias" meaning that -> ReturnThat has.

The exceptional context is in an argument to a function:

fn foo(thing: impl Display) { ... }

That is like using a generic T: Display on foo.[1]


  1. except not as useful due to not being nameable, turbofishable, or compatible with precise capturing ↩ī¸Ž

3 Likes

impl Trait has many meanings depending on its position (which I admit, can be confusing). TL;DR is that fn foo(_: impl Trait) is mostly similar to fn foo<T: Trait>(_: T) (other that you cannot name this type using fully qualified syntax), but fn foo() -> impl Trait means "function foo returns some unnameable type that implements trait Trait".

There is a great talk by John Gjengset explaining all current (and future) meanings of this feature, and differences between them - https://www.youtube.com/watch?v=CWiz_RtA1Hw.

1 Like

Thank you both @quinedot and @akrauze , I didn't even realize that impl can be used as an argument. I'll watch the video today but at this moment generics look like more useful / something to use more often.

Just to clarify. impl Trait in return position is still useful and often used. They just express different things. But if you are talking about function argument position, then I agree. I personally never use impl Trait in this context, because it is strictly less flexible than introducing a named generic type. If you want to deny it in your codebase, there is a clippy lint clippy::impl_trait_in_params, that will force it.

2 Likes