Skip to content

Types

This section is meant to provide extra context and operations to the types you’ve already seen throughout this section.

The Newtype Pattern

Rust allows you to create lightweight wrappers around types that help with ecapsulation and abstraction. To do this you simply declare a tuple struct that names the wrapped type as its only field. The new type does not retain its inner APIs, but you can implement them manually for the new type. You can also derive some. Check with the documentation to avoid any unintended

// Muh newtype
struct Meter(u32);
// Select trait implementations
// Copy and Clone are derivable, but this is more fun
// Implementing Clone requires Copy
impl Copy for Meter {}
impl Clone for Meter {
fn clone(&self) -> Self {
*self
}
}
impl std::ops::Add for Meter {
type Output = u32;
fn add(self, rhs: Self) -> Self::Output {
// Use the 0th index because Meter is a tuple struct
self.0 + rhs.0
}
}
impl std::fmt::Display for Meter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}m", self.0)
}
}
impl PartialOrd for Meter {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.0.cmp(&other.0))
}
}
impl PartialEq for Meter {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
// Calling code
pub fn using_meter() {
let m = Meter(12);
let n = Meter(23);
// The m + n operation requires the Add trait
let sum = Meter(m + n);
// Printing the values without a formatter requires the Display trait
println!("Newtype arithmetic: {} + {} = {}", m, n, sum);
// Comparing the values requires the PartialOrd and PartialEq traits
let larger = if m > n {
m
} else {n};
println!("Of the two elements {} and {}, {} is larger.", m, n, larger);
}
Newtype arithmetic: 12m + 23m = 35m
Of the two elements 12m and 23m, 23m is larger.

Type Aliasing

There are times when a type’s signature is too long for readability. Rust allows yo to alias a type to reduce repition or write more succinct code. In fact the enum Result<T, E> has a std lib alias as Result<T> for exactly this reason. Declare a type alias with the type keyword like type NewName = OldName. The Result<T, E> alias looks like this:

type Result<T> = std::result::Result<T, std::io::Error>;

Type aliasing can turn verbose, difficult to read code into cleaner and easier to read code. Consider the following example:

use std::fmt;
use std::io::Error;
pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize, Error>;
fn flush(&mut self) -> Result<(), Error>;
fn write_all(&mut self, buf: &[u8]) -> Result<(), Error>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<(), Error>;
}

Aliasing the Result type produces signatures that are much easier to read.

pub trait Write {
fn write(&mut self, buf: &[u8]) -> Result<usize>;
fn flush(&mut self) -> Result<()>;
fn write_all(&mut self, buf: &[u8]) -> Result<()>;
fn write_fmt(&mut self, fmt: fmt::Arguments) -> Result<()>;
}

The Rust book’s chapter on server implementation creates a type called Job with a rather verbose and awkward signature. Its used in a closure.

struct Job {
job: Box<dyn FnOnce() + Send + 'static>
}

The type is used in another struct.

pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Job {
job: Box::new(f),
};
match self.sender.send(job) {
Ok(_) => (),
Err(e) => {
println!("Error: {e}");
std::process::exit(1);
}
}
}

Instead of wrapping the type, which is really just functioning as a trait object, you can simply alias the base type and use it directly for convenience.

type Job = Box<dyn FnOnce() + Send + 'static>;
...
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
let job = Box::new(f);
match self.sender.send(job) {
Ok(_) => (),
Err(e) => {
println!("Error: {e}");
std::process::exit(1);
}
}
}

The ! Type

Rust has a type refered to in type theory as the empty type which, surprise, has no values. This is colloquially called the never type in Rust because it is used for functions that return never, or expressions that have no type. The continue keyword is an example of an expression that does not have a type. Additionally panic!() and loop also never have a value or return a type. Expressions that evaluate to ! can be coerced into any type because they dont have a type. The ! type is commonly used for error handling, exhaustive pattern matching, and representing diverging computations.

The never type ! is different from the unit type () in that the unit type is used in expressions and functions that actually return, even if the computation resultsin no meaningful value. The unit type represents exactly one value, which is is kind of like how void is used in other languages. The never type is used in functions and expressions that dont actually return, like panics or infinite loops. This provides a clear indication to both the compiler and developers that normal execution flow will not continue past that point. The unit type canot be coerced into any type like the never type can. The unit type is often the implicit return type for functions that don’t specify a return type. While has a size of zero bytes, it’s still a real type with a value, unlike the never type which has no values at all.

Dynamicly Sized Types

Rust needs to know a type’s size at compile time. Rust forces you to use some sort of indirection when creating dynamically sized types (DSTs). For example, the string slice (str) cannot be bound directly. Instead you must put the str behind a pointer. Instead of storing an unknown size the (fat) pointer stores the starting address and length of the variable’s bound data.

let s: str = "Hello"; // Illegal
let s: &str = "Hello"; // Legal