Generic data types

Getting harder and harder

Short intro

I was busy for the last few weeks. I like to learn fun and geeky stuff, but my job occasionally requires me to learn very new technologies with bad documentation or without it at all. And it’s hard. At such moments I have troubles to learn anything else. But I’ve recently done hard part of magnum/kubernetes madness (hello, Mr. Patchy, I wrote you a patch to make magnum at devstack works, and I done it without knowing what the hell I’m doing…). In spite of all that I got an hour or two for Rust at this weekend.

Generic data types

The book has been noting generics so often, that introduction was hardly a surprise.

My observations:

I could use more than one type in generic. <T1, T2> worked as expected. Oh, and the book provided that example too, so I just jumped ahead. The book gives advise not to do this. I completely agree with this, as the closest thing for ‘generics’ in Python is a nasty *args, **kwargs construction which I loathe (it kills introspection and function signatures!).

The generic in the impl gave me hard time. It sounded completely justified, yet a bit hard to grasp. I even peeked into Rust by Example. Nope, they have a unsatisfyingly small page on generic.

Therefore I found myself at need to play with generics a lot by myself.

I tried. And I was baffled. I’ve stuck. I will write down all my struggles here. Now it’s not just a light-headed blog entry, but my actual laboratory log to store my intermediate results for future analysis. It’s going to be ugly with a lot of boring source code and error citations and mostly without comments.

My initial code, I used to play with methods. It has generic struct but no any methods.

use std::mem;
struct Foo<T>{
x: T
}
fn main() {
let x: Foo<u32> = Foo{x: 2};
let y: Foo<u8> = Foo{x: 3};
println!("size1: {}, size2: {}", mem::size_of_val(&x), mem::size_of_val(&y));
}

output is as expected: size1: 4, size2: 1.

Boilerplate for impl play:

use std::mem;
struct Foo<T>{
x: T
}
// impl for .size goes here
fn main() {
let x: Foo<u32> = Foo{x: 2};
let y: Foo<u8> = Foo{x: 3};
println!("size1: {}, size2: {}", x.size(), y.size());
}

Expectations: the same output, size1: 4, size2: 1.

Puzzle 1

impl<T> Foo<T> {
fn size(&self) -> usize {
mem::size_of_val(&self)
}

Output: size1: 8, size2: 8.

While I’m doing later pieces I realized my error. mem::size_of_val (&self) returns size of reference, which is 64-bits on my machine. I changed it to mem::size_of_val(self) and got a proper result. (4/1). Mystery solved.

Invalid code 1

impl Foo<T> { …

compilation error:

7 | impl Foo<T> {
| ^ not found in this scope

invalid code 2

impl Foo { ...
7 | impl Foo {
| ^^^ expected 1 type argument

invalid code 3

impl<T> Foo<u32> { ...

7 | impl<T> Foo<u32> {
| ^ unconstrained type parameter

Puzzle 2

impl<u32> Foo<u32> {
fn size(self) -> usize {
mem::size_of_val(&self)
}
}

Output size1: 4, size2: 1. I have no objection on 4, but where and how they got 1? I implemented only u32…

Modified version clarified a bit:

impl<u32> Foo<u32> {
fn size(self) -> usize {
// mem::size_of_val(&self)
3
}
}

Output is expected unexpectedness: 3, 3.

Modification #2:

impl<u32> Foo<u32> {
fn size(self) -> usize {
//mem::size_of_val(&self)
3
}
}
impl<u8> Foo<u8> {
fn size(self) -> usize {
//mem::size_of_val(&self)
5
}
}

Which leads to error:

| |___^ duplicate definitions for `size`

I really puzzled here.

… What, <u32> is THE GENERIC.

impl Foo<u32> {
fn size(self) -> usize {
3
}
}
impl Foo<u8> {
fn size(self) -> usize {
5
}
}

Now output is expected expectedness: size1: 3, size2: 5.

Huh, it was a bit of brain storm here. Now when puzzle with <u32> is solved, I can play with normal behavior. I’m interested in combination of impl with generic parameter, without generic parameter and their combination.

After some brainstorm on those errors, here my conclusions:

  1. Impl can be with generic or without. If it is with generic, then type name in the structure name should be the same as in impl’s generic. In this case it is implemented as ‘generic impl’.
  2. Impl without generic should have real type in the struct. (e.g. impl Foo<u8>).
  3. If there are generic impl, there should be no ‘explicit’ impls, they are conflicting.

Those are rules for ‘one type’ generics. Now I’ll try to play with two generic structures…

As usual, my bootstrap code (with simple impl with two generics, it would be a point of my experiments)

use std::mem;
struct Foo<T, U>{
x: T,
y: U
}
impl<T, U> Foo<T, U> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}
fn main() {
let x: Foo<u32, u32> = Foo{x: 2, y:1};
let y: Foo<u8, u8> = Foo{x: 3, y:5};
println!("size1: {}, size2: {}", x.size(), y.size());
}

Expected output: 8, 2.

Switch from generic to explicit impl:

impl Foo<u32, u32> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}
impl Foo<u8, u8> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}

It worked as expected: 8, 2.

Now a hard part. Can I make a ‘half-explicit’ (with only one type been generic)?

Yes, I can:

impl<T> Foo<u32, T> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}
impl<T> Foo<u8, T> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}

Can I use a more complicated mixture? Well, this code does not work:

impl<T> Foo<T, u32> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}
impl<T> Foo<u8, T> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}

rustc complains on duplicate definitions for `size`. I can guess why, but I’m not sure…

My last test:

impl<T> Foo<T, T> {
fn size(&self) -> usize {
mem::size_of_val(&self.x) + mem::size_of_val(&self.y)
}
}

It worked as expected (8/2).

I got some superficial understanding of impl with generics. I can use generic at any type to ‘generalize’ elements inside block. If I use some type in the generic declaration place (impl <T>), it would be used ‘as declared’ in all other entrances of that type (e.g. Foo<T>), and in the code.

I can declare generic with one or more generalizations (types), but if I declare it, I couldn’t declare more specific versions with strict types.

I hope that would be enough…

One last thing, which I didn’t get in the chapter. Can I have normal functions with generics?

use std::mem;
struct Foo<T, U>{
x: T,
y: U
}
fn size<T>(x: &T) -> usize {
mem::size_of_val(x)
}
fn main() {
let x: Foo<u32, u32> = Foo{x: 2, y:1};
let y: Foo<u8, u8> = Foo{x: 3, y:5};
println!("size1: {}, size2: {}", size(&x), size(&y));
}

It worked! I think I start to grasp generics.

My attempt to game the system failed:

fn size<T>(x: &T) -> usize {
mem::size_of_val(&x.x)
}

error: no field `x` on type `&T` (same with size_of_val(x.x)).

Conclusion

I’d like to say something smart here, but I can’t. I barely managed to get something reasonable out of compiler, leave aside something nice. I got impression I need to start to write real code, or I will sink in those ‘theoretical study’. I will finish this chapter, and then I’ll try to write something reasonable without guidelines (but with intensive googling).

Like what you read? Give George Shuklin a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.