📜 ⬆️ ⬇️

On the fingers: the associated types in Rust and how they differ from the type arguments

Why does Rust have associated types, and how do they differ from type arguments aka generics, because they are so similar? Is it not enough just the latter, as in all normal languages? For those who are just starting to study Rust, and especially for people who come from other languages ​​(“It's generics!”, The javist, who has been wise for years), says that this question arises regularly. Let's figure it out.


TL; DR First controls the called code, second - the caller.


Generic vs associated types


So, we already have type arguments, or all beloved generics. It looks like this:


trait Foo<T> { fn bar(self, x: T); } 

Here T is just the type argument. It seems that this should be enough for everyone (like 640 kilobytes of memory). But in Rust there are also associated types, like these:


 trait Foo { type Bar; //    fn bar(self, x: Self::Bar); } 

At first glance, the same eggs, but from a different angle. Why did you need to introduce another entity into the language? (Which, by the way, was not in the early versions of the language.)


Type arguments are exactly the arguments , which means that they are passed to the trait at the call site, and control over which type will be used instead of T belongs to the caller. Even if we do not explicitly specify T in the place of the call, the compiler will do it for us using type inference. That is, implicitly anyway, this type will be displayed on the caller and passed as an argument. (Of course, all this happens at compile time, not in runtime.)


Consider an example. In the standard library there is an AsRef AsRef that allows one type to pretend to be another type for a while, converting the link to itself into a link to something else. Simplified this trait looks like this (in reality it is a bit more complicated, I deliberately removed all unnecessary, leaving only the minimum necessary for understanding):


 trait AsRef<T> { fn as_ref(&self) -> &T; } 

Here, type T passed by the caller as an argument, even if it happens implicitly (if the compiler infers that type for you). In other words, it is the caller who decides what type of T will pretend to be our type that implements this treyt:


 let foo = Foo::new(); let bar: &Bar = foo.as_ref(); 

Here, the compiler, using the knowledge of bar: &Bar , will use the AsRef<Bar> implementation to call the as_ref() method, because it is the type of Bar that the caller needs. Of course, the type Foo should implement the AsRef AsRef<Bar> , and in addition it can implement as many other AsRef<T> variants as possible, among which the caller chooses the right one.


In the case of the associated type, everything is exactly the opposite. The associated type is completely controlled by those who implement this treit, and not the caller.


A common example is an iterator. Suppose we have a collection, and we want to get an iterator from it. What type of values ​​should an iterator return? In exactly the one contained in this collection! The non-caller must decide what the iterator returns, and the iterator itself knows better what it can return. Here is the abbreviated code from the standard library:


 trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } 

Notice that the iterator does not have a type parameter that would allow the caller to choose what the iterator should return. Instead, the type of the value returned from the next() method is determined by the iterator itself using the associated type, but it is not pinned, ie each iterator implementation can choose its own type.


Stop. So what? All the same, it is not clear how this is better than a generic. Imagine for a moment that we use the usual generic instead of the associated type. The iterator iterate will then look something like this:


 trait GenericIterator<T> { fn next(&mut self) -> Option<T>; } 

But now, first, type T to be specified again and again in each place where the iterator is mentioned, and secondly, it has now become possible to implement this treit several times with different types, which looks somewhat strange to the iterator. Here is an example:


 struct MyIterator; impl GenericIterator<i32> for MyIterator { fn next(&mut self) -> Option<i32> { unimplemented!() } } impl GenericIterator<String> for MyIterator { fn next(&mut self) -> Option<String> { unimplemented!() } } fn test() { let mut iter = MyIterator; let lolwhat: Option<_> = iter.next(); // Error! Which impl of GenericIterator to use? } 

See the catch? We cannot simply take and call iter.next() without squats - it is necessary to let the compiler know, explicitly or implicitly, which type will be returned. And it looks awkward: why should we, on the side of the call, know (and tell the compiler!) The type that the iterator returns, whereas how should this iterator know better what type it returns?! And all because we were able to implement the GenericIterator GenericIterator twice with different parameters for the same MyIterator , which, from the point of view of the iterator's semantics, also looks like an absurdity: why is it that the same iterator can return values ​​of different types?


If we go back to the variant with the associated type, then all these problems can be avoided:


 struct MyIter; impl Iterator for MyIter { type Item = String; fn next(&mut self) -> Option<Self::Item> { unimplemented!() } } fn test() { let mut iter = MyIter; let value = iter.next(); } 

Here, firstly, the compiler without unnecessary words correctly displays the type value: Option<String> , and secondly, you will not be able to implement the Iterator MyIter for MyIter second time with a different type of return value, and thus spoil everything.


To secure. A collection can implement such a treyt in order to be able to turn itself into an iterator:


 trait IntoIterator { type Item; type IntoIter: Iterator<Item=Self::Item>; fn into_iter(self) -> Self::IntoIter; } 

And again, it is the collection that decides what kind of iterator it will be, namely: an iterator, the type of the return value of which coincides with the type of the elements of the collection itself, and no other.


Even more "on the fingers"


If the examples above are still not clear, then here is a less scientific but more intelligible explanation. Arguments of types can be considered as "input" information that we provide for the treit to work. Associated types can be viewed as "output" information that the treit provides to us, so that we can use the results of his work.


In the standard library it is possible to overload mathematical operators for their types (addition, subtraction, multiplication, division, and the like). To do this, you need to implement one of the corresponding traits from the standard library. Here, for example, how this trey looks for an addition operation (again, simplified):


 trait Add<RHS> { type Output; fn add(self, rhs: RHS) -> Self::Output; } 

Here we have the "input" argument RHS - this is the type to which we will apply the addition operation with our type. And there is an "output" argument Add::Output - this is the type that will result from the addition. In general, it may differ from the type of the terms, which, in turn, may also be of different types (add tasty to blue, and get soft - but what I do all the time). The first is set with the help of the type argument, the second with the help of the associated type.


You can implement as many additions as you like with different types of the second argument, but each time there will be only one type of result, and it is determined by the implementation of this addition.


Let's try to realize this treyt:


 use std::ops::Add; struct Foo(&'static str); #[derive(PartialEq, Debug)] struct Bar(&'static str, i32); impl Add<i32> for Foo { type Output = Bar; fn add(self, rhs: i32) -> Bar { Bar(self.0, rhs) } } fn test() { let x = Foo("test"); let y = x + 42; //      <Foo as Add>::add(42)  x assert_eq!(y, Bar("test", 42)); } 

In this example, the type of the variable y is determined by the addition algorithm, and not by the calling code. It would be very strange if it were possible to write something like let y: Baz = x + 42 , that is, to force the addition operation to return the result of some extraneous type. It is precisely from such things that the associated type Add::Output insures us.


Total


We use generics where we don’t mind having multiple trait implementations for the same type, and where it’s acceptable to specify a specific implementation on the call side. We use the associated types where we want to have one "canonical" implementation, which itself controls the types. We combine and mix in the right proportions, as in the last example.


Coin failed ? Finish me with comments.


')

Source: https://habr.com/ru/post/441444/


All Articles