Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I provide an implementation of a generic struct in Rust?

I have a struct MyStruct that takes a generic parameter T: SomeTrait, and I want to implement a new method for MyStruct. This works:

/// Constraint for the type parameter `T` in MyStruct
pub trait SomeTrait: Clone {}

/// The struct that I want to construct with `new`
pub struct MyStruct<T: SomeTrait> {
    value: T,
}

fn new<T: SomeTrait>(t: T) -> MyStruct<T> {
    MyStruct { value: t }
}

fn main() {}

I wanted to put the new function inside an impl block like this:

impl MyStruct {
    fn new<T: SomeTrait>(t: T) -> MyStruct<T> {
        MyStruct { value: t }
    }
}

But that fails to compile with:

error[E0107]: wrong number of type arguments: expected 1, found 0
 --> src/main.rs:9:6
  |
9 | impl MyStruct {
  |      ^^^^^^^^ expected 1 type argument

If I try to put it like this:

impl MyStruct<T> {
    fn new(t: T) -> MyStruct<T> {
        MyStruct { value: t }
    }
}

The error changes to:

error[E0412]: cannot find type `T` in this scope
 --> src/main.rs:9:15
  |
9 | impl MyStruct<T> {
  |               ^ not found in this scope

How do I provide an implementation of a generic struct? Where do I put the generic parameters and their constraints?

like image 624
manu Avatar asked Feb 03 '19 14:02

manu


People also ask

How do generics work in Rust?

In Rust, generics refer to the parameterization of data types and traits. Generics allows to write more concise and clean code by reducing code duplication and providing type-safety. The concept of Generics can be applied to methods, functions, structures, enumerations, collections and traits.

What does the T mean in Rust?

Short for “type,” T is the default choice of most Rust programmers. When we use a parameter in the body of the function, we have to declare the parameter name in the signature so the compiler knows what that name means.

What is impl in Rust?

The impl keyword is primarily used to define implementations on types. Inherent implementations are standalone, while trait implementations are used to implement traits for types, or other traits. Functions and consts can both be defined in an implementation.

What are generic associated types?

GATs (generic associated types) were originally proposed in RFC 1598. As said before, they allow you to define type, lifetime, or const generics on associated types. If you're familiar with languages that have "higher-kinded types", then you could call GATs type constructors on traits.


1 Answers

The type parameter <T: SomeTrait> should come right after the impl keyword:

impl<T: SomeTrait> MyStruct<T> {
    fn new(t: T) -> Self {
        MyStruct { value: t }
    }
}

If the list of types and constraints in impl<...> becomes too long, you can use the where-syntax and list the constraints separately:

impl<T> MyStruct<T>
where
    T: SomeTrait,
{
    fn new(t: T) -> Self {
        MyStruct { value: t }
    }
}

Note the usage of Self, which is a shortcut for MyStruct<T> available inside of the impl block.


Remarks

  1. The reason why impl<T> is required is explained in this answer. Essentially, it boils down to the fact that both impl<T> MyStruct<T> and impl MyStruct<T> are valid, but mean different things.

  2. When you move new into the impl block, you should remove the superfluous type parameters, otherwise the interface of your struct will become unusable, as the following example shows:

    // trait SomeTrait and struct MyStruct as above
    // [...]
    
    impl<T> MyStruct<T>
    where
        T: SomeTrait,
    {
        fn new<S: SomeTrait>(t: S) -> MyStruct<S> {
            MyStruct { value: t }
        }
    }
    
    impl SomeTrait for u64 {}
    impl SomeTrait for u128 {}
    
    fn main() {
        // just a demo of problematic code, don't do this!
        let a: MyStruct<u128> = MyStruct::<u64>::new::<u128>(1234);
        //                                 ^
        //                                 |
        //        This is an irrelevant type
        //        that cannot be inferred. Not only will the compiler
        //        force you to provide an irrelevant type, it will also
        //        not prevent you from passing incoherent junk as type
        //        argument, as this example demonstrates. This happens 
        //        because `S` and `T` are completely unrelated.
    }
    
like image 147
Andrey Tyukin Avatar answered Nov 07 '22 03:11

Andrey Tyukin