Genericity is the capacity to write code once, with types not or partly specified, so that the code can be used for many different types. Rust has this capacity in abundance, applying it for both data structures and functions.
A composite data structure is generic if the type of its items can be of a general type <T>. The type T can for example be an i32 value, an f64, a String, but also a struct type like Person that we have coded ourselves. So, we can have a vector Vec<f64>, but also a vector Vec<Person>. If you make T a concrete type, then you must substitute the type T with that type everywhere T appears in the definition of the data structure.
Our data structure can be parametrized with a generic type <T>, and so has multiple concrete definitions--it is polymorphic. Rust makes extensive use of this concept, which we encountered already in Chapter 4, Structuring Data and Matching Patterns when we talked about arrays, vectors, slices and the Result and Option types.
Suppose you want to define a struct with two fields, first and second, but you want to keep the type of these fields generic. We can define it as follows:
// see code in Chapter 5/code/generics.rs struct Pair<T> { first: T, second: T, }
We can now define a Pair of magic numbers, or a Pair of magicians, or whatever we want, like this:
let magic_pair: Pair<u32> = Pair { first: 7, second: 42 }; let pair_of_magicians: Pair<&str> = Pair { first: "Gandalf", second: "Sauron" };
What if we wanted to write functions that work with generic data structures? They would also have to be generic, right? As a simple example, how would we write a function that returns the second item of a Pair? We can do it like this:
fn second<T>(pair: Pair<T>) { pair.second; }
We could also call the second function as follows:
let a = second(magic_pair);
This produces the following output:
42
Let's now investigate why Option and Result types are so powerful. Here is the definition of the Option type again:
enum Option<T> { Some(T), None }
From this, we can define multiple concrete types, as follows:
let x: Option<i8> = Some(5); let pi: Option<f64> = Some(3.14159265359); let none: Option<f64> = None; let none2 = None::<f64>; let name: Option<&str> = Some("Joyce");
When the type does not correspond with the value, a mismatched types error occurs, as in the following code:
let magic: Option<f32> = Some(42)
We can define a struct Person as follows:
struct Person { name: &'static str, id: i32 }
Then, we can add a few Person objects using let binding:
let p1 = Person{name: "James Bond", id: 7}; let p2 = Person{name: "Vin Diesel", id: 12}; let p3 = Person{name: "Robin Hood", id: 42};
Then, we can make an Option type value or a vector for the struct Person as the following:
let op1: Option<Person> = Some(p1); let pvec: Vec<Person> = vec![p2, p3];
You would use the Option type in a situation where you expect to get a value, but when there is always a possibility that no value will be given. A typical scenario would be user input.