How to automatically borrow a fixed-size array as a slice, when calling function that takes a trait

Hi,

This is more of a code-readability question than anything.

I have a trait implemented on a slice

impl Key for &[u8] {}

Thanks to const generics, I could implement the trait on a reference to a fixed array:

impl <const ARRAY_SIZE : usize>Key for &[u8; ARRAY_SIZE] {}

but that implementation becomes problematic when I have methods that return Self. The trait makes sense for a slice, and just doesn't make sense for an array.

So, what I really want is for the compiler to automatically borrow a slice from the array when I call a function that takes a generic trait T as an arg. So the function calls would look like this:

fn key_fn<K : Key>(key : K) {}

fn main() {
    key_fn(b"hello");
}

instead of like this:

    key_fn(&b"hello"[..]);

I've been trying to figure out a helper trait that could make this happen, but so far I keep chasing my tail.

Thank you for any insights.

The problem is that the Rust doesn't perform any automatic type conversion on generic function call, as it would makes adding impl for existing trait on existing type a breaking change, which is not desirable. Possible workaround is to make another trait, say IntoKey, to handle manual conversion.

trait Key { ... }

trait IntoKey {
    type Key: Key;

    fn into_key(self) -> Self::Key;
}

impl<'a> Key for &'a [u8] { ... }

impl<K: Key> IntoKey for K {
    type Key = Self;

    fn into_key(self) -> Self::Key {
        self
    }
}

impl<'a, const SIZE: usize> IntoKey for &'a [u8; SIZE] {
    type Key = &'a [u8];

    fn into_key(self) -> Self::Key {
        &self[..]
    }
}

fn key_fn<K: IntoKey>(key: K) {
    let key = key.into_key();
    ...
}
4 Likes

Thanks for the reply!

What you have written does indeed solve the problem as I asked it. I felt really dumb when I read your answer because I thought I had already tried exactly this before posting the question.

But I felt a little less dumb when I realized that having a generic type argument to the Key and IntoKey traits now makes the generic Key impl conflict with the array impl.

trait Key<KeyCharT> { }

trait IntoKey<KeyCharT> {
    type Key: Key<KeyCharT>;

    fn into_key(self) -> Self::Key;
}

impl<'a, KeyCharT> Key<KeyCharT> for &'a [KeyCharT] { }

impl<KeyCharT, K: Key<KeyCharT>> IntoKey<KeyCharT> for K {
    type Key = Self;

    fn into_key(self) -> Self::Key {
        self
    }
}

impl<'a, KeyCharT, const SIZE: usize> IntoKey<KeyCharT> for &'a [u8; SIZE] {
    type Key = &'a [u8];

    fn into_key(self) -> Self::Key {
        &self[..]
    }
}

fn key_fn<K: IntoKey<u8>>(key: K) {
    let key = key.into_key();
    
}

Is there a way around this other than to mirror every impl of Key for IntoKey?

Thanks again!

Does it make sense if a certain type has multiple Key impl with different KeyCharT parameter? If not, it would be better to make it as associate type instead of the generic parameter, which makes my example works mostly as-is(after fixing some typo :P).

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f84d174952800954574cf2ab4ca72e87

2 Likes

I think this is the best explanation for the question: When should I use a generic trait parameter and when should I use an associated type?

This question previously confused me badly, so, if I may paraphrase what you said:

If impl Trait<TypeA> for T and impl Trait<TypeB> for T might coexist, then use a generic type parameter. But if the T in impl Trait for T implies the other type, then use an associated type.

It was a long and arduous process, but I reworked my whole project to transition the KeyCharT from a generic trait argument to an Associated Type, and now your solution works perfectly.

Was it worth it for the tiny improvement in readability? Debatable. Was it worth it to level-up my Rust understanding surrounding associated types? 100% Yes!

Thank you!

4 Likes

May you clarify what you mean by "implies the other type"?

My use of the word "implies" was conceptual. Specifically, in my case, I had an Associated Type (KeyCharT), the value of which was implied by the type the trait was implemented on. So:

impl Key for &[u8] {
    type KeyCharT = u8;
}

or

impl Key for Vec<u8> {
    type KeyCharT = u8;
}

In my example, there can only be one reasonable value for KeyCharT and therefore:

impl Key<u8> for Vec<u8> {
}

is redundant and needlessly limiting. (This is the implementation I had when I asked the question)

But going for a more self-contained rule-of-thumb, It is basically what @Hyeonu wrote. So, I'll further paraphrase it as:

If impl Trait<TypeA> for T and impl Trait<TypeB> for T both make sense on their own and might coexist together, then use a generic type parameter. But if only one impl Trait for T makes sense for each T then use an associated type.

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.