3 Compound data types

Last edited

Return Types

Special return types

  • () ‘unit type’ formally as a zero-length tuple. Use when you’re returning nothing
  • ! ’ this function never returns (infinite loop, panics)

struct

Example

#[derive(Debug)] // like a decorator? which allows for repr? {:?}
struct File {
    name: String,
    data: Vec<u8>,
}

fn main() {
    let f1 = File {
        name: String::from("f1.txt"),
        data: Vec::new(),
    };

    // We want to access this data by reference
    // f1_name is 'borrowing' the data these refer to.
    let f1_name = &f1.name;
    let f1_length = &f1.data.len();

    println!("{:?}", f1);
    println!("{} is {} bytes long", f1_name, f1_length)
}

file_struct

Type alias’

Downside, each new type (like our struct) must opt in to all of its intended behavior from String.

// Hostname is a considered a newtype
struct Hostname(String);

fn connect(host: Hostname) {
    println!("connected to {}", host.0);
}

fn main() {
    let ordinary_string = String::from("localhost");
    let host = Hostname(ordinary_string.clone());

    connect(ordinary_string);
}
// Had we defined Hostname like this, it would accept a string as well.
type Hostname(String)

Methods with impl (implementation)

Rust has no class keyword, types are made with struct and methods are created with impl.

impl

new() pattern

Very common way to build a constructor, effectively a method to run __init__. Callers can also just use the literal themselves.

struct File {
    name: String,
    data: Vec<u8>,
}

impl File {
    fn new(name: &str) -> File {
        File {
            name: String::from(name),
            data: Vec::new(), // do some lifting for the caller
        }
    }
}

let f3 = File::new("f3.txt");

File implementation

We’ve been building this throughout, here’s our file struct with its methods

#![allow(unused_variables)]
#[derive(Debug)] // compile macro, that generates basically __repr__
struct File {
    name: String,
    data: Vec<u8>,
}

impl File {
    fn new(name: &str) -> File {
        File {
            name: String::from(name),
            data: Vec::new(),
        }
    }

    fn new_with_data(name: &str, data: &Vec<u8>) -> File {
        let mut f = File::new(name);
        f.data = data.clone();
        f 
    }

    fn read(self: &File, save_to: &mut Vec<u8>) -> usize {
        let mut tmp = self.data.clone();
        let read_length = tmp.len();
        save_to.reserve(read_length);
        save_to.append(&mut tmp);
        read_length
    }
}

fn open(f: &mut File) -> bool {
  true
}
 
fn close(f: &mut File) -> bool {
  true
}

fn main() {
    let f3_data: Vec<u8> = vec![114, 117, 115, 116, 33];
    let mut f3 = File::new_with_data("2.txt", &f3_data);

    let mut buffer: Vec<u8> = vec![];

    open(&mut f3);
    let f3_length = f3.read(&mut buffer);
    close(&mut f3);

    let text = String::from_utf8_lossy(&buffer);

    println!("{:?}", f3);
    println!("{} is {} bytes long", &f3.name, f3_length);
    println!("{}", text);
}

Returning errors

Note

unsafe - Consider unsafe to be a warning sign rather than an indicator that you’re embarking on anything illegal. Unsafe means “the same level of safety offered by C at all times.”

Global mutable variables for errors

C style programs often use the pattern below, this is not recommended in rust, but it’s something to know as you may need to deal with it.

use rand::{random};

// mutable global variables are denoted with static mut
static mut ERROR: isize = 0;

struct File;
 
#[allow(unused_variables)]
fn read(f: &File, save_to: &mut Vec<u8>) -> usize {
    // This function simulates what glibc' read would do
    if random() && random() && random() {
        unsafe {
            ERROR = 1;
        }
    }
    0
}
 
#[allow(unused_mut)]                     
fn main() {
    let mut f = File;
    let mut buffer = vec![];
 
    read(&f, &mut buffer);
    // accessing *static mut variables* is an unsafe operation
    unsafe {
        if ERROR != 0 {
            panic!("An error has occurred!")
        }
    }
}
  • By convention, global variables use ALL CAPS.
  • A const keyword is included for values that never change.

Question

What’s the difference between const and let? if by default let is immutable why would I need const?

Answer

data behind let can change, some types make it look like they’re immutable but change in the background.

let relates more to aliasing than immutability. Aliasing in compiler terminology refers to having multiple references to the same location in memory at the same time.

Making use of the Result return type

This sounds a lot like Go.

Rust’s approach to error handling is to use a type that stands for both the standard case and the error case. This type is known as Result. Result has two states, Ok and Err.

fn double(s: &str) -> Result<i32, std::num::ParseIntError> {
    let n = s.parse::<i32>()?;
    Ok(n * 2)
}

Go’s equivlant

func double(s string) (int, error) {
	n, err := strconv.Atoi(s)
	if err != nil {
		return 0, err
	}
	return n * 2, nil
}

Unwrap()

Calling functions that return Result<File, String> requires an extra method (unwrap()) to actually extract the value. The unwrap() call unwraps Ok(File) to produce File. It will crash the program if it encounters Err(String)

Warning

Calling .unwrap() on a Result is often considered poor style. When called on an error type, the program crashes without a helpful error message

Enum

Simple example of a log level parser.

#[derive(Debug)] 
enum Event {
    Update,
    Delete,
    Unknown,
}

type Message = String;

fn parse_log(line: &str) -> (Event, Message) {
    // Create a vector from
    let parts: Vec<_> = line.splitn(2, ' ').collect();

    if parts.len() == 1 {
        return (Event::Unknown, String::from(line));
    }

    let event = parts[0];
    let rest = String::from(parts[1]);

    match event {
        "UPDATE" | "update" => (Event::Update, rest),
        "DELETE" | "delete" => (Event::Delete, rest),
        _ => (Event::Unknown, String::from(line)),
    }
}

fn main() {
    let log = "BEGIN Transaction XK342
UPDATE 234:LS/32231 {\"price\": 31.00} -> {\"price\": 40.00}
DELETE 342:LO/22111";

    for line in log.lines() {
        let parse_result = parse_log(line);
        println!("{:?}", parse_result);
    }
}

Traits

Simple trait example, basically define an interface using the trait keyword and the methods the type must implement.

// We're saying to be a Read trait you must implement the following function signatures.
trait Read {
    fn read(self: &Self, save_to: &mut Vec<u8>) -> Result<usize, String>;
}

struct File;

impl Read for File {
    fn read(self: &File, save_to: &mut Vec<u8>) -> Result<usize, String> {
        Ok(0)
    }
}

fn main() {
    let f = File {};
    let mut buffer = vec![];
    let n_bytes = f.read(&mut buffer).unwrap();
    println!("{} byte(s) read from {:?}", n_bytes, f);
}

std::fmt::Display for your own types

Basically we’ve been inherting __repr__ on our types.

We’ve relied on #[derive(Debug)] to implement the Display method on our type, but we can implement it ourself for own custom type.

Implementing the Display trait (basically __str__)

#![allow(dead_code)]

use core::fmt;
use std::fmt::{Display}; // brings display into scope, avoiding the need to prefix it


#[derive(PartialEq)]
enum FileState {
  Open,
  Closed,
}
 
struct File {
  name: String,
  data: Vec<u8>,
  state: FileState,
}

impl File {
    fn new(name: &str) -> File  {
        File{
            name: String::from(name),
            data: Vec::new(),
            state: FileState::Closed,
        }
    }
}

impl Display for FileState {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            FileState::Open => write!(f, "OPEN"),
            FileState::Closed => write!(f, "CLOSED"),
        }
    }
}

impl Display for File {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        // self.state will call the Display implementation of FileState
        // This should display: '<f6.txt (CLOSED)>'
        write!(f, "<{} ({})>", self.name, self.state)
    }
}
 
 
fn main() {
  let f5 = File::new("f5.txt");
  println!("{}", f5);
}

Information Hiding Crates

By default everything is private. Use the pub keyword to make things public.

#[derive(Debug,PartialEq)]
pub enum FileState {
    // ..
}
 
#[derive(Debug)]
pub struct File {
  // ..
}

impl File {
    // Even though the struct is public it's methods must be explicitly
    // marked as public.
    pub fn new(name: &str) -> File {
    // ..
    }
}

Documentation

Lexicon

/// Points down at the next item
//! Points up at its container. (typically at the top of a file or module block)

Example

//! Simulating files one step at a time.


impl File {
    /// Creates a new, empty `File`.
    ///
    /// # Examples
    ///
    /// ```
    /// let f = File::new("f1.txt");
    /// ```
    pub fn new(name: &str) -> File {
        File {
            name: String::from(name),
            data: Vec::new(),
        }
    }
}

Commands to generate documentation

cargo doc --no-deps --open