[Rust] Type system

Brian Pan
6 min readNov 27, 2020

Convenient ways of type conversions

  • T is Deref<U> : *(T) -> U
  • T is AsRef<U>: &T -> &U
  • T is Borrow<Borrowed>: save cost to allow some convenient ways during function calls. T can be borrowed by some types of Borrowed

Deref

  • & -> get reference of the var
  • * -> deref an address

Without the Deref trait, the compiler can only dereference & references. The deref method gives the compiler the ability to take a value of any type that implements Deref and call the deref method to get a & reference that it knows how to dereference.

The reason the deref method returns a reference to a value and that the plain dereference outside the parentheses in *(y.deref()) is still necessary is the ownership system. If the deref method returned the value directly instead of a reference to the value, the value would be moved out of self. We don’t want to take ownership of the inner value inside MyBox<T> in this case or in most cases where we use the dereference operator.

// def of the trait Deref
type trait Deref {
type Target : ?Sized;
fn deref(&self) -> &Self::Target;
}
// impl example
impl ops::Deref for String {
type Target = str;
fn deref(&self) -> &str {
unsafe {
str::from_utf8_unchecked(&self.vec)
}
}
}
// cast &String to &str
fn main() {
let x = "hello".to_string();
// or use x.deref()
// *x -> *(x.deref()) -> str type
match &*x {
"hello" => {println!("!")},
_ -> {}
}
}

AsRef

AsRef is a convenient to do reference to reference conversion.

Good for a wrapper struct to collect its inner type.

pub trait AsRef<T>
where T: ?Sized, {
fn as_ref(&self) -> &T;
}

Borrow

MIT reference

Borrow trait allows caller to supply any one of multiple essentially identically variants of the same type.

Types express that they can be borrowed as some type T by implementing Borrow<T>, providing a reference to a T in the trait’s borrow method. A type is free to borrow as several different types.

Borrow trait has a blanket implementation of Borrow<T> for T, &T, &mut T

pub trait Borrow<Borrowed> {
fn borrow(&self) -> &Borrowed;
}

Ex: hashmap

HashMap<K, V> owns both keys and values. If the key’s actual data is wrapped in a managing type of some kind, it should, however, still be possible to search for a value using a reference to the key’s data. For instance, if the key is a string, then it is likely stored with the hash map as a String, while it should be possible to search using a &str

use std::borrow::Borrow;
use std::hash::Hash;

pub struct HashMap<K, V> {
// omitted
}

impl<K, V> HashMap<K, V> {
pub fn insert(&self, key: K, value: V) -> Option<T>
where K: Hash + Eq
{}

pub fn get<Q>(&self, k: &Q) -> Option<&V>
where
K: Borrow<Q>,
Q: Hash + Eq + Sized
{
}
}

// the reason why we can use hashmap.get("str")
impl Borrow<str> for String

ToOwned

The trait to copy data from borrowed data

pub trait ToOwned {
type Owned: Borrow<Self>;
// create owned data from borrowed data
fn to_owned(&self) -> Self::Owned;
// use borrowed data to replace owned data
fn clone_into(&self, target: &mut Self::Owned){..}
}
// String implement Borrow<str>
let s: &str = "a";
let ss: String = s.to_owned();

let mut s: String;
"hello".clone_into(&mut s); // s becomes "hello"

Traits as the tags

implementing a trait to a struct is somewhat like labeling a tag on it

  • Sized :Types ensure the size during compiled period
  • Unsize: Types that can be “unsized” to a dynamically-sized type.
  • Copy: Types whose values can be duplicated simply by copying bits.
  • Clone: A common trait for the ability to explicitly duplicate an object.
  • Send: Types that can be transferred across thread boundaries.
  • Sync: Types share ref safely between threads
pub trait Clone : Sized {
fn clone(&self) -> Self;
fn clone_from(&mut self, source: &Self){
*self = source.clone();
}
}
// example of implementing Clone trait
impl Clone for T {
fn clone(&self) -> T {
*self
}
}
#[derive(Copy, Clone)]
struct TStruct;
let t = TStruct{};
let tc = t.clone(); // call clone

Traits for Comparison

  • PartialEq, Eq : (a == b, b == a), (a == b, b == c, a == c) for equality (to use assert_eq!)
pub trait Eq : PartialEq<Self> {}
  • PartialOrd : trait for values to be compared in a sorted order. it implements partial_cmp, le, lt, ge, gt
pub enum Ordering {
Less,
Equal,
Greater,
}
pub trait PartialOrd<Rhs=Self> : PartialEq<Rhs>
where Rhs : ?Sized,
{
fn partial_cmp(&self, other: &Rhs) -> Option<Ordering>;
fn le(&self, other: &Rhs) -> bool;
fn lt(&self, other: &Rhs) -> bool;
fn ge(&self, other: &Rhs) -> bool;
fn gt(&self, other: &Rhs) -> bool;
}
  • Ord: The type forms a total order. It implements max, min, cmp,lamp . you must define an implementation for cmp for trait Ord
pub trait Ord : Eq + PartialOrd<Self> {
fn cmp(&self, other: &Self) -> Ordering;
fn max(&self, other: &Self) -> Self;
fn min(&self, other: &Self) -> Self;
fn clamp(&self, min: &Self, max: &Self) -> Self{..}

Trait from & into

pub trait From<T> {
fn from(T) -> Self;
}
pub trait Into<T> {
fn into(self) -> T;
}
impl<T, U> Into<U> for T where U : From<T>;// String & str
fn main() {
let a_string = "hello".to_string();
let b_string = String::from("hello");
}

Trait Iterator

trait Iterator {
type Item;
fn next(&mut self) -> Option<Self::Item>;
}
impl Iterator for LimitCounter{
type Item = usize;

fn next(&mut self) -> Option<usize> {
self.start += 1;
if self.start <= self.limit {
Some(self.start)
} else {
None
}
}
}
// step by step in each iteration
pub struct Step<I> {
iter: I,
skip: usize,
}
// impl Iterator for Step
impl<I> Iterator for Step<I>
where I: Iterator {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
let e = self.iter.next();
if self.skip > 0 {
self.iter.nth(self.skip - 1);
}
e
}
}
// interface / wrapper for Step
pub fn step<I>(iter: I, skip: usize) -> Step<I>
where I : Iterator,
{
assert!(skip != 0);
Step{
iter: iter,
skip: skip,
}
}
//impl step for all Iterator
pub trait IterExt : Iterator {
fn step(self, n: usize) -> Step<Self>
where Self: Sized,
{
step(self, n)
}
}
impl<T: ?Sized> IterExt for T where T: Iterator{}
fn main() {
let mut ct = LimitCounter{start: 0, limit: 2};
println!("{}", ct.next().unwrap());

let arr = [1,2,3,4,5];
// iter(): yeild &T
// iter_mut(): yield &mut T
// into_iter(): Yeild &T, &mutT, T depends on the context
let sum = arr.iter().step(1).fold(0, |acc, x| acc + x);
}

iter() family

  • IntoIter: transfer the ownershop: &self
  • Iter: get immutable reference: &self
  • IterMut: get mutable reference: &mut self
iter() -> &T
iter_mut() -> &mut T
into_iter() -> T, &T, or &mut T depending on the context
struct Foo {
bar: Vec<u32>,
}
impl Foo {
fn all_zeros(&self) -> bool {
self.bar.into_iter().all(|x| x == 0) // can't do this because move semantic
}

iterator manipulation

// use inspect to check the item repsectively
let nvec = vec![1,2,3];
let dvec = nvec.iter()
.map(|i| i*2)
.inspect(|it| println!("the item is {}", it))
.collect<Vec<i32>>();

Trait as Abstract Type

Trait itself can be a type. However, the size of it can’t be defined during compile time. That is why we have to use pointer/reference when using trait objects.

trait Bar {
fn baz(&self);
}
// must use ptr or reference since the size of the object is not defined during compile time
fn dynamic_dispatch(b: &Bar) {
t.baz()
}
fn static_dispatch(b: impl Bar) {
b.baz()
}
// trait like structure
// https://pabloariasal.github.io/2017/06/10/understanding-virtual-tables/
pub struct TraitObject {
pub data: *mut (),
pub vtable: *mut (), // concept of virtual table from C++ which includes information of methods, destructors,...
}

Trait blanket

implement several structs for trait at once

trait Fooer: Copu + Clone + Ord + Bar {}

struct Foo<F: Fooer> {
vals: Vec<F>,
}

// implement ToString trait for all structs implementing Display and ?Sized
impl<T> ToString for T where
T: Display + ?Sized,
{ ... }

--

--