Summary of 2nd Lecture

While reading this summary find the code examples here.

Enums and pattern matching

Enums are called sum types while structs are called product types, because a value of an enum can be one of it's variants while the value of a struct should have all the fields filled up which is essentially a cartesian product (discrete maths!).

A enum of the form

enum Expr {
    Integer(i32),
    Add(Box<Expr>, Box<Expr>),
    Sub(Box<Expr>, Box<Expr>),
}

has type name Expr and three constructors Integer, Add and Sub. So a value of type Expr can constructed using one of the above 3 variants and hence can be destructured using one of those three variants.

How enums are laid out in memory can be found here.

In Rust Box is used to allocate a block of memory in the heap. One simply has to do something like let reference = Box::new(some_value). Here the value held by the variable reference is very similar to what we see in a typical OOP language. Read more about stack vs heap in Chapter 4 of Rust book here.

Why Box is required to define recursive types?

As you can observe Expr is a recursive type as Add and Sub takes arguments of type Box<Expr>. But why Box is needed? Rust like C++ will try to allocate values as much in the Stack as possible, because it is much faster and cheaper to maintain (OS does all the work!). In order to allocate a value in the Stack Rust needs to know the size of the type at compile time. There are sizes of types which can't be known at compile time like dynamically allocated array (Vec in Rust), HashMap etc.,

But the pointer to those objects is typically the address of the first byte which is nothing but usize in Rust.

The result of the Box is just a pointer and hence the size of Box<Expr> is the size of usize, which is typically 32 or 64 bits based on the CPU architecture.

Pattern Matching on Enums

The matching for the Expr enum is demonstrated below, Note that the destructured value will transfer the ownership of the data to the associated variable names in the destructors.

fn evaluate_expr(expr: Expr) -> i32 {
    match expr {
        Expr::Integer(value) => value,
        Expr::Add(e1, e2) => evaluate_expr(*e1) + evaluate_expr(*e2),
        Expr::Sub(e1, e2) => evaluate_expr(*e1) - evaluate_expr(*e2),
    }
}

The * operator which is called dereferencing or unboxing in Rust allows us to get the data pointed to by the reference, remember Box is just a reference.

Method call syntax

Using the impl keyword we can associate functions and methods to a type. Examples,

struct Point {
    x: i32,
    y: i32,
}

impl Point {
    // Associated function
    fn new(x: i32, y: i32) -> Point {
        Point {x, y}
    }

    // Associated method which takes the ownership of the Point value
    fn lost(self) {
        println!("point {:?} will be destroyed when this function terminates", self);
    }

    // Associated method which takes the mutable reference of the Point value
    fn set_x(&mut self, new_x: i32) {
        self.x = new_x;
    }

    // Associated method which takes the read only reference of the Point value
    fn get_y(&self) -> i32 {
        self.y
    } 
}

Traits

Traits provides a mechanism to unify different types which behaves in a certain way, that is according to the trait defintion. Examples,

struct Circle {
    center: Point,
    radius: u32,
}

struct Square {
    center: Point,
    side: u32,
}

trait Shape {
    fn area(&self) -> u32;
    fn perimeter(&self) -> u32;
}

impl Shape for Square {
    fn area(&self) -> u32 {
        self.side * self.side
    }
    fn perimeter(&self) -> u32 {
        self.side * 4
    }
}

impl Shape for Circle {
    // Let the value of pi be 3 for illustration 
    fn area(&self) -> u32 {
        3 * self.radius * self.radius
    }

    fn perimeter(&self) -> u32 {
        2 * 3 * self.radius
    }
}

Now we have two types Square and Circle which are distinct in nature but both of them implements the Shape trait, i.e they share a common behaviour.

Now any function that expects a value of type Shape as input can be used by the both of the types.

Then we discussed how to implement a simple enough Singly Linked List in Rust and implemented the Iterator Trait for that List so that it can use all the functions defined for the Iterator trait. Refer the source code where I have written extensive comments.

Read more about Linked List in Rust here. Completing this will give you thorough understanding of memory allocations in Rust.

Last updated