Home

Programming With Rust

A small Rust project and programming concepts.

Overview

In this project I built a small issue tracker in Rust to demonstrate and practise some fundamental programming concepts while also putting my passive knowledge of programming languages, and Rust in particular, to use. This program will store a collection of Issues, display their state, and allow individual issues to be marked as fixed.

Issue Representation

Each issue is represented using a struct:

struct Issue {
    number: u32,
    description: String,
    is_fixed: bool,
}           

A structs purpose is to group related data into a single type. In this example, every issue here contains an ID number, a description, and a boolean representing whether the issue has been "fixed". The description value is stored as a String, allowing each issue ownership of its text.

Now to make creating issues simpler and more consistent, I added an associated ::new() function for our newly created type:

impl Issue {
    fn new(number: u32, description: String) -> Issue {
        Issue {
            number,
            description,
            is_fixed: false,
        }
    }
}           

Therefore allowing me to create an Issue with Issue::new(), this also ensures that every newly created Issue begins with the is_fixed boolean set to false.

Methods and Borrowing

Here I added some methods inside impl to "fix" and print each issue's values:

fn fix(&mut self) {
    self.is_fixed = true;
}

fn print(&self) {
    println!(
        "#{} | {} | Fixed?: {}",
        self.number,
        self.description,
        self.is_fixed
    );
}           

The two methods here will also demonstrate the difference between immutable and mutable borrowing in Rust. print() uses &self because of its need to only read the issue, while fix() uses &mut self since it needs to change the value of is_fixed. In short: &self has read-only access, whilst &mut self has permission to modify; all this is happening while the issue being borrowed is still owned by the code that created it; the methods used here only borrow access for as long as is needed.

A Collection of Issues

Here I store the issues in a vector:

let mut issues: Vec<Issue> = Vec::new();

Vec<Issue> is a growable collection that contains Issue values. The vector itself is also mutable, as we will be pushing new issues to it during the program.

To display all the stored issues in the collection, I pass the collection to a function and then have it iterate over its contents:

fn print_issues(issues: &[Issue]) {
    for issue in issues {
        issue.print();
    }
}           

This function only needs to read the issues, therefore only requiring a immutably borrowed slice rather than a mutable one.

And contrasting the immutably borrowed slice, is this mutably borrowed one:

fn fix_issue(issues: &mut [Issue], number: u32) {
    for issue in issues {
        if issue.number == number {
            issue.fix();
            return;
        }
    }

    println!("Can't find issue!");
}           

This function will check each issue until it finds the matching number. Once the number has been found, it will call the fix method and return immediately. However, if the loop finishes and does not find a match, the program reports that the issue could not be found.

Putting Everything Together

Issues are created using Vec::new() and will be one-by-one pushed into a vector:

let mut issues: Vec<Issue> = Vec::new();

issues.push(Issue::new(
    1,
    String::from("wifi isnt working!!"),
));
issues.push(Issue::new(
    2,
    String::from("browser wont open"),
));
issues.push(Issue::new(
    3,
    String::from("daves doing that thing again..."),
));         

Then the same created collection, can be borrowed differently depending on what the program will require:

println!("Issues before:");
print_issues(&issues);

fix_issue(&mut issues, 2);

println!("Issues after:");
print_issues(&issues)

Here, print_issues(&issues) only needs to borrow the collection for reading, whereas fix_issue(&mut issues, 2) receives a mutable borrow, thus allowing changes to the stored Issue's inside issues.

Running the program shows the second issue's state change:

Issues before:
#1 | Wi-Fi isn't working | Fixed?: false
#2 | Browser won't open | Fixed?: false
#3 | Dave's doing that thing again | Fixed?: false
Issues after:
#1 | Wi-Fi isn't working | Fixed?: false
#2 | Browser won't open | Fixed?: true
#3 | Dave's doing that thing again | Fixed?: false

What I Learned

This project and my recent general study solified a lot of programming concepts I had floating around in my head for years, i.e., completed a few internet C, Python, JavaScript, Java (etc.) courses.

Here and recently I was able to solidify what I knew and actually apply that knowledge to something tangible, while also learning about Rust's interesting features such as borrowing and ownership.