The Front Door to Rust

Most documentation of and tutorials for Rust start right off with working on the stack using just &references and no traditional pointers / references whatsoever.

Usually, how to use pointers is deferred to later chapters, even after explaining 'lifetime specifiers.

Most programmers, though, structure their programs using something akin to objects, which also in most cases use internally shared instances of other objects.

So let's take a look at Rust as an imperative language that easily facilitates using objects to make the first steps more easy.

Rust's functional programming features feel much more enticing once it's possible to use them from a program that is structured in a comfortable way.

Full code for the following examples:

labyrinth.rs

map.rs

Enter Reference Counting

struct Labyrinth {
    map: Ref<Map>,
    player: Ref<Player>,
    solver: Ref<Solver>
}

impl Labyrinth {
    fn new() -> Labyrinth {
        let map = new(Map::new(0, 0));
        let player = new(Player { x: 0, y: 0, map: map.clone()});
        let solver = new(Solver { map: map.clone(), player: player.clone()});
        return Labyrinth {
            map,
            player,
            solver
        };
    }

    fn solve(&self) {
        self.solver.borrow_mut().solve()
    }
}

This is a rudimentary playground for solving a labyrinth.

The Player instance is parameterized with the same Map instance as the Labyrinth instance itself.

The Solver instance is parameterized with the same Map and Player instances as the Labyrinth instance itself.

There is just one problem: This code does not compile as it is.

Rust uses libraries to provide things like reference counting and using them directly leads to extremely verbose syntax.

In this case, a few use statements plus one typedef and one function make it work without impeding all of your code's readability more than necessary:

use std::cell::RefCell;
use std::rc::Rc;
use crate::map::Map;

type Ref<T> = Rc<RefCell<T>>;

fn new<T>(value: T) -> Ref<T> {
    return Rc::new(RefCell::new(value))
}

The third include is for Map, which is no different than Player or Solver except that is it sitting in its own file.

ref and new are thus not built-in keywords but just shortcuts to make the code more readable.

I guess Rust will get something like classes, similar to the way Javascript did, in the future as syntactic sugar but for now it's good to know that we can use struct & impl to create objects with the same flexibility we're used to from other languages.

Why Reference Counting

Raw pointers and garbage collected references exist in Rust but raw pointers kinda turn your code into C code.

Garbage collection, on the other hand, removes the advantage of Rust of a language with hardly any runtime overhead compared to C or C++, plus memory and resource management become indeterministic, therefore it is not the obvious choice for a language aiming at competing in the low-level arena.

Using garbage collected references is very similar to using reference counting.

Reference counting requires thinking about how to structure objects without creating cyclic references but otherwise is automatic and deterministic. Garbage collection can be significantly faster for burst operations followed by pauses. Referencing and dereferencing a lot inside an inner loop yields a heavy burden for the machine, so it is best to only use reference counting for the larger structure of a program and rely on the stack and the borrow checker, and raw pointers if necessary, for e.g. number crunching.

What's Next?

Now that this approach has hopefully made it easier to actually get started and Rust looks much more like a normal proramming language, just learn all that makes Rust special.

All this article aims to do is giving you a comfy chair to do that from 😎