Costrain generalized method to only primitive types

Hi all,

I wrote a little testcode to dump a i32 value as binary to a file (verifiable via hexdump) and read it back in (Playground).

The procedure would be the same for u32, i64, ... and so on. Therefore I would like to use generics to make it easier.
Fist I defined a trait with the general type T:

trait WriteByte<T> {
    fn write_be_binary(&mut self, val: &T);
}

Next I need to implement the write_be_binary method:

impl<T> WriteByte<T> for File {
    fn write_be_binary(&mut self, val: &T) {
        let buf = val.to_be_bytes();
        self.write_all(&buf).expect("Unable to write to file");
    }   
}

Obviously the 'to_be_bytes` is not implemented for all types but only for the primitives (to_be_bytes()). Rightfully the compiler complains:

error[E0599]: no method named `to_be_bytes` found for reference `&T` in the current scope
  --> src/main.rs:10:23
   |
10 |         let buf = val.to_be_bytes();
   |                       ^^^^^^^^^^^ method not found in `&T`

For more information about this error, try `rustc --explain E0599`.
error: could not compile `readwritebytes` (bin "readwritebytes") due to previous error

Which constraint do I need to use in the generic implementation to limit T to primitive types?

Thanks for your help.

You probably don't want to do that. It seems to me like the capability you need is "convert to bytes in a lossless manner". You should be thinking primarily in terms of capabilities, and not in terms of closed sets of types.

That said, there's num_traits::ToBytes.

However, for integer types only, you can do this using nothing but bit operations and the corresponding traits from the standard library: Playground

fn to_be_bytes<T>(x: T, buf: &mut [u8])
where
    T: Copy + From<u8> + BitAnd<T, Output = T> + Shr<usize, Output = T>,
    u8: TryFrom<T>,
    <u8 as TryFrom<T>>::Error: Debug,
{
    let mask = T::from(0xff);

    for (i, place) in (0..size_of::<T>()).rev().zip(buf) {
        *place = u8::try_from((x >> i * 8) & mask).expect("_ & 0xff does not fit into byte");
    }
}

For even more general conversions, use e.g. bytemuck::bytes_of().

7 Likes

Thanks for the quick help.
It makes total sense to think in capabilities and not sets of types here.
After I constrained T to ToBytes, the argument of the write_all method does not fit anymore:

error[E0308]: mismatched types
  --> src/main.rs:12:24
   |
12 |         self.write_all(&buf).expect("Unable to write to file");
   |              --------- ^^^^ expected `&[u8]`, found `&<T as ToBytes>::Bytes`
   |              |
   |              arguments to this method are incorrect
   |
   = note: expected reference `&[u8]`
              found reference `&<T as ToBytes>::Bytes`
   = help: consider constraining the associated type `<T as ToBytes>::Bytes` to `[u8]`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

I understand the problem, but I fail to see how I would realize the constraint of <T as ToBytes>::Bytes to [u8]

You don't, because it's never a slice – it's a sized array, of which the size depends on the type of the input type.

Please read the documentation of the trait, that's why I linked to it. Bytes has a supertrait of AsRef<[u8]>.

Sorry I find this extremely confusing.
I read the documentation you linked, but as with a lot of documentation for Rust it is really hard to extract any value from it.
I understand that write all takes a [u8] slice. And from the documentation you linked I can see that to_be_types returns Self::Bytes. But if Bytes is different from a [u8] slice/array, why does the assert_eq in the example passes?

use num_traits::ToBytes;

let bytes = ToBytes::to_be_bytes(&0x12345678u32);
assert_eq!(bytes, [0x12, 0x34, 0x56, 0x78]);

First argument is type Bytes from the num_traits crate and the second one is a sized array of type u8.
Why can't I iterate over the Bytes type, if it is similar to a [u8]?

I probably misunderstood something again, but as I said, I think rust docs are highly confusing.

And Self::Bytes is required to implement the NumBytes trait, which in turn has supertraits AsRef<[u8]> and even Borrow<[u8]>.

It's not different from an array. It is an array. It's just not a slice.

An array is not a slice, and a slice is not an array. An array is [T; N] where N is a fixed size, and a slice is a [T] where the size is not known statically. A slice therefore can't be returned from a function by-value; hence the need for an associated type (the size of an array is part of its type, so the type must depend on the input type T, which is exactly what associated types are for).

So, in your code, bytes has type [u8; 4], which can be equated to another [u8; 4] (the 0x12, 0x34, etc. literal).

That said, this is because the example you cited above has all concrete types. Within a generic function, you can't know what the concrete associated type must be, hence the only thing you can do with it is call methods specified by its trait bounds.

What is confusing in any of the docs linked above? Everything is written down in them explicitly.

3 Likes

Maybe I am not experienced enough to understand all this, but I still fail to make the generic function work.

I'm getting more and more frustrated with rust.

As mentioned, you need to call as_ref(). If you want something else, please specify it.

2 Likes

Thank you for your Help. I have much to learn.
One thing that I would like to know is: How did you know which trait is needed as a constraint for the to_be_bytes method. When searching to_be_bytes on docs.rs I only get a huge list of crates. Is there some way to find this info, or is it something that you will know after working with rust for a while?

If the question is "what trait I need for expressing a primitive numerical operation", then the answer is almost invariably the num_traits crate.

I mean, how do you know that you need Serde for serialization, rusqlite for an SQLite wrapper, or rand for random number generation? The same is true for the num family of crates – they are a well-established, reputable, central source of numeric operations.

1 Like

Thank you.

I guess I just have to get familiar with the different crates and experiment a lot.

You can also ask the community for the most popular crate(s) for a given task.

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.