Advanced Types
Uses of the Newtype Pattern
- Type Safety: We can wrap
u32values with structsMillimetersandMeters. Now, if a value is stored inMillimeters, it is safe to say that this value can't call functions defined forMeters, and vice versa is true. - Abstraction: We could provide a
Peopletype to wrap aHashMap<i32, String>that stores a person’s ID associated with their name. Code usingPeoplewould only interact with the public API we provide, such as a method to add a name string to the People collection; that code wouldn’t need to know that we assign ani32ID to names internally.
Type Aliases
-
This is how you can create a type alias:
#![allow(unused)] fn main() { type Kilometers = i32; } -
How it works?
- Values with type
Kilometerswill be treated same asi32. - We aren't creating a new type, we're just adding a synonym to
i32, calledKilometers
- Values with type
-
Hence, doing something like this is totally fine:
#![allow(unused)] fn main() { let x: i32 = 5; let y: Kilometers = 5; println!("x + y = {}", x + y); // This will work } -
The advantage is that it will give us the flexibility that any function with an argument of
i32, we can passKilometersvalue to it. -
The disadvantage is that we don't get the type checks as we get in the newtype pattern.
-
You can create an alias where you want to prevent naming a complex type.
#![allow(unused)] fn main() { type Thunk = Box<dyn Fn() + Send + 'static>; // A type that stores closure let f: Thunk = Box::new(|| println!("hi")); // We don't need to specify the longer type, instead we can say just "Thunk" fn takes_long_type(f: Thunk) { // Again, we don't need to specify the longer type, just "Thunk" ... } }
Fun Fact: Thunk is a word for code to be evaluated at a later time, so it’s an appropriate name for a closure that gets stored
-
We can also shorten a
Resultvalues of I/O operations like this#![allow(unused)] fn main() { type Result<T> = std::result::Result<T, std::io::Error>; } -
Now,
Result<usize, Error>can be replaced withResult<usize>andResult<(), Error>can be replaced withResult<()>. Also, we can use the?operator, since it's the same type.
The never type !
-
This type never returns. In type theory lingo it is known as the empty type because it has no values.
-
Rust prefers to call it the never type because it stands in the place of the return type when a function will never return.
#![allow(unused)] fn main() { // People read it as, "the function bar(), returns never", and are called diverging functions fn bar() -> ! { // --snip-- } } -
Rust never allows a variable to have different possible data types.
#![allow(unused)] fn main() { // FAIL: You can't create a variable "guess", that may have either number or string let guess = match guess.trim().parse() { Ok(_) => 5, Err(_) => "hello", }; } -
But this is possible:
#![allow(unused)] fn main() { let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => continue, // Wait a minute, how's this even allowed? }; } -
The
continuehas a never type (!), which means it'll never return any value. That is, when Rust computes the type ofguess, it looks at both match arms, the former with a value ofu32and the latter with a!value. Because!can never have a value, Rust decides that the type of guess isu32. -
You can remember it this way, the
!can get coerced to any other type. -
This is the original implementation of
unwrap!(). Here, the return type is coerced to a single typeT, and that's becausepanic!()ends the program and has a never type (!).#![allow(unused)] fn main() { impl<T> Option<T> { pub fn unwrap(self) -> T { match self { Some(val) => val, None => panic!("called `Option::unwrap()` on a `None` value"), } } } } -
The value of the expression of
loopis also of the type!.#![allow(unused)] fn main() { print!("forever "); loop { print!("and ever "); } }
Dynamically Sized Traits and the Sized trait
-
DSTs or unsized types let us write code using values whose size can only be known at runtime.
-
So, Rust doesn't let us create strings with
str(not&str):#![allow(unused)] fn main() { let s1: str = "Hello there!"; let s2: str = "How's it going?"; } -
Why's that?
- Rust needs to know a fixed size of a type. Here
s1takes 12 bytes ands2takes15bytes. - It's not possible to accomodate all the strings in a single fixed size.
- Rust needs to know a fixed size of a type. Here
-
What's the solution?
&stris the solution.- It stores two values: the address of the
strand its length. - So, that makes
&strwill only need twousize, one for the address and the other for the length. - That's why, we always know the size of a
&str, no matter how long the string it refers to is.
-
In general, this is the way in which dynamically sized types are used in Rust.
-
The golden rule of dynamically sized types is that we must always put values of dynamically sized types behind a pointer of some kind.
-
The traits can be Dynamically Sixed too. All we need to do is to put them behind a pointer, such as
&dyn TraitorBox<dyn Trait>. -
The
Sizedtrait-
A trait that determines whether or not a type's size is known at compile time.
-
You may create a generic function like this:
#![allow(unused)] fn main() { fn generic<T>(t: T) { // --snip-- } } -
But, Rust treats it as if it was re-written like this:
#![allow(unused)] fn main() { // This means generic functions will only work on // types who's size is known at the compile time fn generic<T: Sized>(t: T) { // --snip-- } } -
It's possible to get over with this restriction:
#![allow(unused)] fn main() { // The ?Sized means “T may or may not be Sized” // Now, this fn will accept T whose size may or may not be known at compile time // The ?Trait syntax with this meaning is only available for Sized, not any other traits. // Also, notice, we're using `&T` and not `T`, now we'll use `T` behind some kind of pointer, here it's reference fn generic<T: ?Sized>(t: &T) { // --snip-- } }
-