Advanced Types in Rust
Newtype Pattern
In previous section we learned about Newtype Pattern in the context of implementing a trait on a given type.
In this example we want to implement Display
trait on a Vector
type, however both are defined outisde of our trait. We get around this by defining a new Wrapper type, which a tuple struct containing a vector.
use std::fmt;
struct Wrapper(Vec<String>);
impl fmt::Display for Wrapper {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn main() {
let w = Wrapper(
vec![String::from("hello"), String::from("world")]
);
println!("w = {}", w);
}
Other uses for Newtype Pattern can be to increase type safety.
For example, we have two functions, one function took an age as a paramter and another function took in an employee id as a parameter. The type of both parameters is u32
. To avoid mixing of age and employee id when calling the function then we can create a new type which wraps an u32
.
Like, we can construct tuple struct for Age
and ID
:
Another use of Newtype Pattern is to abstract away implementation details.
For example, we can create People
type which wraps a hash map of integers to strings.
In an essence, Newtype Pattern is a lightweight way to achieve encapsulation.
Type Aliases
Rust also allows to create type aliases to give existing types new names.
fn main() {
type Kilometers = i32; // not a new type; just a synonym
let x: i32 = 5;
let y: Kilometers = 5;
println!("x + y = {}", x + y);
}
The main usecase of type aliases is to reduce repetition:
fn main() {
// `f` of a very lengthy type
let f: Box<dyn Fn() + Send + 'static> =
Box::new(|| println!("hi"));
fn takes_long_type(f: Box<dyn Fn() + Send 'static>) {
// --snip--
}
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> {
// --snip--
}
}
Instead of writing this type over and over again, we can create type alias:
// much easier to read & write
fn main() {
type Thunk = Box<dyn Fn() + Send + 'static>;
let f: Thunk = Box::new(|| println!("hi"));
fn takes_long_type(f: Thunk) {
// --snip--
}
fn returns_long_type() -> Thunk {
// --snip--
}
}
Type aliases also convey meaning for the type. For example, above here Thunk
here is a word for code that will be evaluated at some later point.
Never Type
The Never Type is a special type denoted with !
meaning that the the function will never return.
Why this might be useful?
Recall that in Chapter 2, we built a guessing game and we had some code which parsed user input into an integer, something like this:
fn main() {
while game_in_progress {
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
}
}
guess
and parse it and return a u32
however if the parsing failed we call continue
to skip this current iteration.
But here guess
is u32
, so how one match
arm returns a u32
and other with continue
.
That's because continue
has a Never Type. Rust will loook at both arms of this match expression:
Thus. Rust confirms that the return type of this expression is an u32
integer.
If we get an Err
variant in the above case, continue
will not return anything, instead it will move the control back to the top of the loop, never assigning to guess
and only assigning u32
.
*Never Typeis useful with
panic!` macro as well.
For example the Option<T>
enum has a method called unwrap()
like this which evaluates self
:
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val, // returns val if `Some` variant
None => panic!( // otherwise panic! won't return anything from this function
"called `Option::unwrap()` on a `None` value"
),
}
}
}
panic!
returns a Never Type.
A loop also has Never Type
However this wouldn't be true if we hadbreak
statement inside of the loop because break
will cause the loop to terminate.
print!
is another macro with similar function to println!
but without any line ending.
Dynamically Sized Types
Dynamically Sized Types or Usigned Type are types whose size we can only know at runtime.
Example of this is str
type.
fn main() {
let s1: str = "Hello there!";
// ^^ error: the size for values of type `str` cannot be known at compilation time
let s2: str = "How's it going?";
}
For both s1
and s2
Rust can't determine the size of these store types at compile time. If we tried to compile this, we'll get the above error.
Instead we need to use borrowed version of str
, &str
:
The string slice &str
stores two values:
1. An address pointing to the location of string in memory
2. The length of the string
Both the address value and the length of the string have a type of usize
, we know their size at compile time.
This is how dynamically sized types are used in Rust. They have extra data structure which stores the size of the dynamic information.
The golden rule for DSTs is that we should always put them behind some sort of pointer..
In previous example str
was behind &
(Reference) but Box<T>
or Rc<T>
would also have just worked fine.
Traits and Dynamically Sized Types
Traits are also Dynamically Sized Types. Traits object always needs to be behind some sort of pointer.
Rust has a special trait called the size trait to determine whether a type Sized
can be known at compile time or not. The Sized
trait is autmatically implemented for every type whose size is known at compile time.
Rust implicitly adds the Sized
trait bound to every generic function.
Rust will automatically add Sized
trait bound like so:
By default generic functions will only work on types whose size is known at compile time, however we ca use the belo syntax to relax this retriction: