Mastering Rust
上QQ阅读APP看书,第一时间看更新

Generics

From the dawn of high-level programming languages, the pursuit of better abstraction is something that language designers have always strived for. As such, many ideas concerning code reuse emerged. The very first of them was functions. Functions allow you to chunk away a sequence of instructions within a named entity that can be called later many times, optionally accepting any arguments for each invocation. They reduce code complexity and amplify readability. However, functions can only get you so far. If you have a function, say avg, that calculates the average of a given list of integer values and later you have a use case where you need to calculate the average for a list of float values too, then the usual solution is to create a new function that can average float values from the list of floats. What if you wanted to accept a list of double values too? We probably need to write another function again. Writing the same function over and over again that differs only by its arguments is a waste of precious time for programmers. To reduce this repetition, language designers wanted a way to express code so that the avg function can be written in a way that accepts multiple types, a generic function, and thus the idea of generic programming, or generics, was born. Having functions that can take more than one type is one of the features of generic programming, and there are other places that generics can be used. We'll explore all of them in this section.

Generic programming is a technique that is only applicable in the case of statically typed programming languages. They first appeared in ML, a statically typed functional language. Dynamic languages such as Python use duck typing, where APIs treat arguments based on what they can do rather than what they are, so they don't rely on generics. Generics are part of the language design feature that enables code reuse and the Don't repeat yourself (DRY) principle. Using this technique, you can write algorithms, functions, methods, and types with placeholders for types, and specify a type variable (with a single letter, which is usually T, K, or V by convention) on these types, telling the compiler to fill in the actual types later when any code instantiates them. These types are referred to as generic types or items. The single letter symbols such as T on type are called generic type parameters. They are substituted with concrete types such as  u32 when you use or instantiate any generic item.

Note: By substitution, we mean that every time a generic item is used with a concrete type, a specialized copy of that code is generated at compile time with the type variable T, getting replaced with the concrete type. This process of generating specialized functions with concrete types at compile time is called monomorphization, which is the procedure of doing the opposite of polymorphic functions.

Let's look at some of the existing generic types from the Rust standard library.

The Vec<T> type from the standard library is a generic type that is defined as follows:

pub struct Vec<T> {
buf: RawVec<T>,
len: usize,
}

We can see that the type signature of Vec contains a type parameter T after its name, surrounded by a pair of angle brackets < >. Its member field, buf, is a generic type as well, and so the Vec itself has to be generic. If we don't have T on our generic type Vec<T>, even though we have a T on its buf field, we get the following error:

error[E0412]: cannot find type `T` in this scope

This T needs to be part of the type definition for Vec. So, when we denote a Vec, we always refer to it by using Vec<T> when denoting generically or by using Vec<u64> when we know the concrete type. Next, let's look at how to create our own generic types.