Are there any problems that existential types solve that can not be solved (elegantly) using other techniques?

In this blog post where existential types finally clicked for me, the author solves his opening problem seemingly without using existential types, or at least, without the fn() -> impl SomeTrait syntax, instead opting for the associated types feature.

Are there some use cases where one would use existential types over associated types for traits?

The primary one I’ve run into is about setting expectations for authors that interact with my code. Each implementation of the trait needs to publically specify every associated type for that trait. This raises the possibility that some client of my API ends up relying on some feature of that associated type that I considered an implementation detail, which limits my ability to make changes.

2 Likes

If you're returning something with closures, it's impossible to name that directly in an associated type. If it has no captures, the closure can be cast to a plain function pointer, fn(Arg) -> Output, but otherwise you'd have to use dynamic dispatch, Box<dyn Fn(Arg) -> Output>. Existential types let you directly return a type even with such closures.

1 Like

@cuviper could you add a short code snippet demonstrating this?

You cannot write out the type in this snippet:

fn my_iter() -> impl Iterator<Item = u32> {
    (0..100).map(|i| 2*i)
}

Right, so that can be written instead:

fn my_iter() -> Map<Range<u32>, fn(u32) -> u32> {
    (0..100).map(|i| 2*i)
}

That's a little worse because the function pointer makes an indirect call, but it's manageable. With enough inlining, it might still optimize to a direct/inlined call.

Now add something captured in that closure:

fn my_iter(x: u32) -> impl Iterator<Item = u32> {
    (0..100).map(move |i| x*i)
}

To specify concrete types for that, you'd have to use a trait object:

fn my_iter(x: u32) -> Map<Range<u32>, Box<dyn Fn(u32) -> u32>> {
    (0..100).map(Box::new(move |i| x*i))
}

You could also return Box<dyn Iterator<Item = u32>>, but either way, we have type erasure.

Sure, but it is not a direct translation, because fn(u32) -> u32 is not the type of the closure as used by the impl Trait syntax.

That's why I pointed out that it adds an indirect call -- sorry if that point wasn't clear.

It's also worse in the sense of exposing what should be an implementation detail i.e. the name of the type rather than just a declaration that there is a type returned that meets the contract specified by the trait (which is of course the obvious part of what existential types / ET accomplish).

The reason it's worse is because it allows for other implementation details to leak through (e.g. inherent methods on the type implementing Iterator), which is impossible with ETs.

A well known downside of ETs that's hopefully resolved at some point not too far from now is that the feature can't be used to e.g. name the type of a field or a local.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.