Advanced Types
Uses of the Newtype Pattern
- Type Safety: We can wrap
u32
values with structsMillimeters
andMeters
. 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
People
type to wrap aHashMap<i32, String>
that stores a person’s ID associated with their name. Code usingPeople
would 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 ani32
ID 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
Kilometers
will 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 passKilometers
value 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
Result
values 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
continue
has 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 ofu32
and 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
loop
is 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
s1
takes 12 bytes ands2
takes15
bytes. - 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?
&str
is the solution.- It stores two values: the address of the
str
and its length. - So, that makes
&str
will 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 Trait
orBox<dyn Trait>
. -
The
Sized
trait-
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-- } }
-