When is it wise to use a linked type compared to a generic type?

There was a problem in this question that can be solved by changing the attempt to use the parameter of the generic type to the associated type. This raised the question, “Why is the associated type more appropriate here?”, Which made me want to know more.

The RFC, which introduced related types, says:

This RFC clarifies feature matching:

  • Treat all trait type parameters as input types, and
  • Providing related types that are output types.

The RFC uses the graph structure as a motivating example, and it is also used in the documentation , but I admit that I have not fully appreciated the benefits of the version of the associated type over the version with type parameterization. The main thing is that the distance method does not care about the type of Edge . This is good, but it seems a bit superficial reason for having related types in general.

I found that related types are fairly intuitive to use in practice, but I have difficulty deciding where and when I should use them in my own API.

When I write code, when should I choose a related type instead of a generic type parameter, and when should I do the opposite?

+81
types idiomatic rust
Aug 17 '15 at 20:44
source share
3 answers

This is now covered in the second edition of The Rust Programming Language . However, let's go a little deeper.

Let's start with a simpler example.

When is it appropriate to use the trait method?

There are several ways to provide late binding:

 trait MyTrait { fn hello_word(&self) -> String; } 

Or:

 struct MyTrait<T> { t: T, hello_world: fn(&T) -> String, } impl<T> MyTrait<T> { fn new(t: T, hello_world: fn(&T) -> String) -> MyTrait<T>; fn hello_world(&self) -> String { (self.hello_world)(self.t) } } 

Despite any implementation / performance strategy, both of the above snippets allow the user to dynamically indicate how hello_world should behave.

The only difference (semantically) is that the trait implementation ensures that for a given type of T that implements trait , hello_world will always have the same behavior, while the struct implementation allows you to have different behavior as a basis.

Whether it is appropriate to use the method depends on the use case!

When is it appropriate to use a related type?

Like the trait methods described above, the associated type is a late-binding form (although this happens during compilation), allowing the trait user to specify for the given instance which type to replace. This is not the only way (so the question is):

 trait MyTrait { type Return; fn hello_world(&self) -> Self::Return; } 

Or:

 trait MyTrait<Return> { fn hello_world(&Self) -> Return; } 

Equivalent to late binding of the above methods:

  • the first ensures that for a given Self there is one Return associated
  • instead, the second allows you to implement MyTrait for Self for multiple MyTrait

Which form is more appropriate depends on whether it makes sense to apply uniqueness or not. For example:

  • Deref uses a bound type because without uniqueness the compiler would go crazy during output
  • Add uses a linked type because its author thought that given two arguments there would be a boolean return type

As you can see, while Deref is an obvious use case (technical limitation), the Add case is less clear: it might be advisable for i32 + i32 give either i32 or Complex<i32> depending on the context? Nevertheless, the author used his judgment and decided that overloading of the return type was not necessary for additions.

My personal position is that there is no right answer. However, in addition to the unicity argument, I would mention that related types simplify the use of a trait, since they reduce the number of parameters that must be specified, so if the flexibility of using a regular attribute parameter is not obvious, I suggest starting with a related type .

+52
Aug 18 '15 at 7:00
source share

Linked types are a grouping mechanism, so they should be used when it makes sense to group types together.

Graph trait introduced in the documentation is an example of this. You want the Graph be universal, but if you have a specific Graph type, you don't need the Node or Edge types to change anymore. A particular Graph does not want to change these types within the same implementation and actually wants them to always be the same. They are grouped, or you can even say, connected.

+28
Aug 17 '15 at 20:58
source share

I come from the world of Swift / iOS, but I think these concepts apply:

  • For a single function, you should use generics to limit several function parameters.

  • For a type, you must use a linked type so that you can restrict the parameters to several functions of that type.

0
Jul 27 '17 at 21:00
source share



All Articles