Non-changable associated type


#1

The associated_type_defaults (tracked at https://github.com/rust-lang/rust/issues/29661) is about allowing defaults for associated types. In the issue some discussion has happened about problems with default methods if the associated type is overriden.

What I would like to propose is some way for a default associated type that can be used in default methods but cannot be overriden. The motivating example is this:

I want to extend the Digest trait from the digest crate with a convenience method. Any use of a digest function will have an associated (in spirit, not currently possible in Rust afaiui) output type for Hash values, namely generic_array::GenericArray<u8, Self as digest::FixedOutput>::OutputSize. Being able to specify this type as an associated type would save me from using an external type alias that’s generic over Digests and that I have to keep updated here

Perhaps a good name for this would be associated type alias? Not sure. Anyway, could adding this feature introduce any of the soundness issues that are preventing associated_type_defaults from moving forwards? I think this suggestion here is sufficiently different that it could be implemented independently, perhaps with different syntax to differentiate it from specifying a default?


#2

If I understood your intention correctly, you can write the following code with the stable Rust:

use digest::Digest;
use digest::generic_array::GenericArray;

trait DigestExt: Digest {
    type Hash;

    // convenience methods here
}

impl<D: Digest> DigestExt for D {
    type Hash = GenericArray<u8, D::OutputSize>;

    // implement methods generically
}

BTW can you share with us that kind of convenience method you want to define?


#3

Oh, you are indeed right; thank you! The convenience method is related to my library logic (a merkle tree where concatenating hashes, prepending a 0x01 byte, then hashing the result is a very common occurence).


#4

Hm, actually I think that ends up not doing everything I want. DigestExt::Hash ends up not implementing serde::Deserialize<’_> and I don’t know how to add that constraint to the DigestExt specification. If it were possible to specify how Hash would look for all impls of DigestExt I think that could work better. Unless this is also possible (I would really like to avoid adding a lifetime parameter to DigestExt everywhere though)?


#5

Try to enable serde feature for generic-array crate. But are you sure you don’t want to use hex representation of the hash value? If I am not mistaken using serde you’ll serialize Hash into array representation, i.e. you’ll get “[255, 0, 255]” and not “ff00ff”.


#6

I have that feature already enabled, it isn’t what I was getting at. By having Hash as a type alias I know it impls Deserialize<'de>. Basically what I want is an associated type alias I think, where such impls would be clear (Digest::Hash would impl Deserialize<'de> because GenericArray has such an impl). Specifying the same with trait bounds becomes very cumbersome because you need to add a lifetime onto Digest itself for the constraints that have an explicit lifetime. Hash or GenericArray do not suffer this fate at all because the lifetimes can be “hidden” in the impls for those traits, but they would appear in the constraint as far as I understand it


#7

Yeah, I think you need associated type constructors here to hang the lifetime parameter on the associated type rather than the trait itself.


#8

That still wouldn’t be the ideal solution I think. What I would like is more like an “associated type alias” that is fixed for all implementations (but can make use of generics for its definition)


#9

I guess this is because rust used the same syntax for both “type alias” and “associated type”.

In modules or functions, type A = B; means type alias; but in traits, type A = B is an associated type.

If I want to create a type alias in a trait for multiple methods, there is no way. I agree with shahn that it is better if we can distinguish these two usage:

trait T {
   type AssocT;
   typedef AliasT = ... ;
}

#10

If you want a type alias, put it in the same module as the trait?

type Hash<Size> = GenericArray<u8, Size>;

trait DigestExt: Digest {
     fn etc() -> Hash<Self::OutputSize>;
}

If the item isn’t polymorphic (that is, impls cannot select it), there’s no reason to put it in the trait body.


#11

This should be resolved when inherent associated types are implemented.

trait DigestExt: Digest {
     fn etc() -> Self::Hash<Self::OutputSize>;
}

impl DigestExt {
    type Hash<Size> = GenericArray<u8, Size>;
}