Improve readibility in return types composed of chain of associated types

Here is a function I ended up writing in my code (or almost this function, I changed the domain of application to not add cognitive complexity, I guess everyone knows about species and animals).

fn species(
    &self,
) -> <<Self::SubFamilyType as SubFamilyTrait>::FamilyType as FamilyTrait>::SpeciesType {
    self.sub_family().family().species()
}

So, of course, this function is not alone, it is part of a collection of traits and you're missing the context here. Take a look at this playground for more details.

So here is my question: Is there a way to write this return type in sections/intermediate types, in order to make it more readable?

Actually, in my real code, the chain of associated types was so long that I even made Clippy complained with type_complexity. And Clippy gives a possible solution: define types.

But in this case, I don't really see how I can define a type:

  • since I depend on Self I cannot make a simple type IntermediateType1 = ... outside of the trait
  • and inside the trait, this would be another associated trait which raises 2 problems:
    • it would show in the documentation (but this is basically noise since it's already defined somewhere else)
    • I would need to define it as, in Animal for example: type FamilyType = <Self::SubFamilyType as SubFamily>::FamilyType;, but this gives me the following error :wink:
error[E0658]: associated type defaults are unstable
  --> src/lib.rs:22:5
   |
22 |     type FamilyType = <Self::SubFamilyType as SubFamily>::FamilyType;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: see issue #29661 <https://github.com/rust-lang/rust/issues/29661> for more information

error: aborting due to previous error

And I don't really see if and how I could leverage generics to name the intermediate types...

Any ideas?

What happens if you enable the unstable feature?

#![feature(associated_type_defaults)]

I didn’t try that for two reasons so far: it needs Nightly (and I'd like to be able to work with Stable) and this feature seems highly unstable and is not moving forward since a long time (I opened the Github issue and almost none of the necessary items are done).

That said, I still tries because, @zireael9797, you're right, at the very least, I should have started with that :smiley:

So I modified the trait SubFamily into the following (see the entire playground here).

trait SubFamily {
    type FamilyType: Family;
    type SpeciesType = <Self::FamilyType as Family>::SpeciesType;
    fn name(&self) -> &str;
    fn family(&self) -> Self::FamilyType;
    fn species(&self) -> Self::SpeciesType {
        self.family().species()
    }
}

And here is the error I get.

error[E0308]: mismatched types
  --> src/lib.rs:18:9
   |
17 |     fn species(&self) -> Self::SpeciesType {
   |                          ----------------- expected `<Self as SubFamily>::SpeciesType` because of return type
18 |         self.family().species()
   |         ^^^^^^^^^^^^^^^^^^^^^^^ expected SubFamily::SpeciesType, found Family::SpeciesType
   |
   = note: expected associated type `<Self as SubFamily>::SpeciesType`
              found associated type `<<Self as SubFamily>::FamilyType as Family>::SpeciesType`

So the compiler doesn't seem to understand correctly the constraints I'm defining (because, if I'm not wrong, the expected associated type and the found associated type are totally equivalent but the compiler doesn't see it).

How about this? (using a much better name than S<T>, hopefully)

type S<T> = <<<T as Animal>::SubFamilyType as SubFamily>::FamilyType as Family>::SpeciesType;

trait Animal {
    ...
    fn species(
        &self,
    ) -> S<Self> { ... }
}

You're kinda making a tree (as in the data structure) here where different methods return a pointer to their ancestor types, with the tree's depth being enforced at compile time by using a different trait for each level.

What about making a more general tree node trait and using that to traverse up and down? I'm guessing the different levels in your tree will have different properties though, so it may be difficult to represent them all with a single trait.

Another thought... Do you need to use traits? If there's only one instance of this type hierarchy, you could use the concrete types directly (see Code Smell: Concrete Abstraction).

2 Likes

Hi @Michael-F-Bryan, thank you for this nice solution. I did try it and it seems to work (see playground). I'm still trying to find a good name for these alias types though :smiley: Although, since they're only alias, they are not used in the documentation (the documentation expand the entire type). So this solution may help the developer to maintain code, but not necessarily the user of these traits and library.

As for your other remarks, you're right to mention them. However, as mentioned in my first post, I simplified the code compared to my real implementation (because there was business-specific objects that I would have had to explain, lifetimes everywhere that were just noise, etc...). Below are some more details.

Concrete types instead of traits

In my case, the traits I'm defining are really supposed to be implemented for different use cases and I don't even know how much would be possible (I know a few but I'm sure I can't be exhaustive). In this case, I believe trait is the right answer. And also, I'm applying a Visitor Pattern and took inspiration from Serde, in particular, the traits I'm talking about looks a lot like MapAccess or SeqAccess.

Generic tree

I did try something like some generic trait TreeNode at some point but ended up with a much more complex code that didn't even compile. I believe the reason is that it needs to be generic over traits (Animal, Family), not over concrete types. Because in this case, being generic over the concrete types means I let whoever implement the concrete types Animal, Family, etc. deal with the implementation of trait TreeNode. But as the library author, I believed it provided a poor experience because I already know from the traits how to get a Species from an Animal so I'd like to be able to provide that implementation automatically for the user. Therefore, trait TreeNode needs to be automatically implemented for every type that implements Animal, Family, etc. But I was not able to make that work on my project. I might give another try later.

If your type alias is public, the rustdoc uses this type alias instead. For example many IO functions of the stdlib presents itself as returning a std::io::Result<T> instead of the std::result::Result<T, std::io::Error>.

1 Like

@Hyeonu Indeed, thanks for the tip.

It'd be really nice if we could return impl Trait from a trait method so the compiler could hide the long complicated type signature. Then you'd have a nice experience for both the author and consumer.

That's not implemented (not even in nightly), and may or may not even work for you, depending on if some places using these traits need to know more information about the associated type (e.g. fn foo<A>() where A: Animal, <A::SubFamilyType as SubFamily>::FamilyType: Bar)... So that line of thought isn't overly helpful at the moment :disappointed:

1 Like