Learning Rust

by Robin G. Allen

Last updated: 2016-03-19.

RSS feed available.


Inspired by Artyom's Learning Racket series, I've decided to log my efforts in learning Rust. I'm going to document my learning process as I go about trying to build a roguelike in Rust. I've downloaded the compiler, skimmed the getting started guide, and written “Hello World”. So let's get started!

For those following along at home: I started out using Rust 0.11.0 but upgraded to the latest version frequently. At the top of each part you'll see the Rust version I was using at the time.

Contents

Part 1
Part 2
Part 3
Part 4
Part 5

Part 1

2014-11-02 — Using Rust 0.11.0

So after successfully getting a “Hello World” compiling, I'll start to turn the program into the beginnings of a roguelike. First, let's just try to get a simple grid of characters displaying.

Quick look at the Rust manual for how to do looping, and let's also separate this stuff into a draw() function:

fn draw() {
    for x in range(0i, 10i) {
        for y in range(0i, 10i) {
            print!("#");
        }
        print!("\n");
    }
}

fn main() {
    draw();
}

Compile:

main.rs:4:9: 4:10 warning: unused variable: `x`, #[warn(unused_variable)] on by default
main.rs:4     for x in range(0i, 10i) {
                  ^
main.rs:5:13: 5:14 warning: unused variable: `y`, #[warn(unused_variable)] on by default
main.rs:5         for y in range(0i, 10i) {
                      ^

OK, so we aren't using our x and y variables. This suggests there must be some way to declare that we aren't interested in their values. Let's try _ à la Scala:

fn draw() {
    for _ in range(0i, 10i) {
        for _ in range(0i, 10i) {
            print!("#");
        }
        print!("\n");
    }
}

Alright! No more warnings.

To keep things simple for now, let's store our level as a map from coordinates to lists of objects. So, e.g., [3,2] → [floor, player]; [4, 16] → [wall].

Google “rust datatypes”
Google “rust maps”
Google “rust language maps”
Google “rust language dictionaries”
Google “rust language hashmaps”

Bingo! Found a relevant-looking article, Getting Rusty.

So we have mutable and immutable HashMaps. Let's try and use immutable data structures where we can. The article suggests creating an immutable map by creating a mutable map and “freezing” it. Let's copy-paste the code:

fn main() {
    let mut map = HashMap<~str, uint>::new();
    draw();
}

Compile:

main.rs:13:28: 13:31 error: obsolete syntax: `~` notation for owned pointer allocation
main.rs:13     let mut map = HashMap<~str, uint>::new();
                                     ^~~
note: use the `box` operator instead of `~`
main.rs:13:31: 13:32 error: expected `;` but found `,`
main.rs:13     let mut map = HashMap<~str, uint>::new();

Nope, I have to use the “box operator” instead of ~. Let's guess:

let mut map = HashMap<box str, uint>::new();

Compile:

main.rs:13:19: 13:26 error: unresolved name `HashMap`.
main.rs:13     let mut map = HashMap<box str; uint>::new();
                             ^~~~~~~
main.rs:13:31: 13:34 error: unresolved name `str`.
main.rs:13     let mut map = HashMap<box str; uint>::new();
                                         ^~~
main.rs:13:36: 13:40 error: unresolved name `uint`. Did you mean `map`?
main.rs:13     let mut map = HashMap<box str; uint>::new();
                                         ^~~~
main.rs:13:41: 13:46 error: unresolved name `new`. Did you mean `map`?
main.rs:13     let mut map = HashMap<box str; uint>::new();
                                                     ^~~~~
error: aborting due to 4 previous errors

So it looks like this guide is pretty out-of-date. Luckily it links to the authoritative docs on HashMaps, so let's go there. 404. OK, let's look up HashMaps in the standard library docs ourselves.

Alright. Ctrl-F “hashmap”. Nope. “hash”? That's the hash, but where's the map? Aha, “collections”. Maybe it's in there? It is! So we want std::collections::hashmap::HashMap; I think it's use to import things?

use std::collections::hashmap::HashMap;

Nope, better look it up.

use std::collections::hashmap::HashMap as HashMap;

Compile:

main.rs:2:40: 2:42 error: expected `;` but found `as`
main.rs:2 use std::collections::hashmap::HashMap as HashMap;

Huh, okay. Am I looking at the right docs? Ah, no, the docs are for 0.13.0 and Homebrew has installed 0.11.0. OK, so let's read the 0.11.0 docs. Can I just change the version in the URL to 0.11.0? No. Google “rust 0.11.0 docs”. OK, here we go.

Ctrl-F “Modules”. Nope.
Ctrl-F “Import”. Nope.
Ctrl-F “Use”. Nope.

OK, “30 minute Rust introduction”. If the module system doesn't have its own page, it must be covered in here. Ctrl-F “use”. Gotcha.

So this page has an example with use std::sync::Arc. Let's see if we can import that.

use std::sync::Arc;

fn main() {
    let numbers = vec![1i, 2i, 3i];
    let numbers = Arc::new(numbers);
}

Well, that compiles. So why can't I use HashMaps? Back to the docs. Google “rust 0.11.0 hashmaps”. Nope. And I can't access the 0.11.0 docs from the Rust site either (though I know they are there somewhere). Luckily the URL is still in my history. But the main “Rust 0.11.0” link on that page takes us all the way back to rust-lang.org so let's try some URL surgery. Alright, table of contents.

So, let's go to Standard library. Collections. We're here. But where is HashMap? I see SmallIntMap, TreeMap, TrieMap, and a Map trait. Let's try Map. The page says:

Implementors:
    impl<V> Map<uint, V> for SmallIntMap<V>
    impl<K: Ord, V> Map<K, V> for TreeMap<K, V>
    impl<T> Map<uint, T> for TrieMap<T>

Hmm. OK, let's try MutableMap. No HashMap there either. Weird, I was sure the 0.11.0 30-minute intro mentioned HashMaps. Ah no, it was the containers page. Still, there it is:

The standard library provides three owned map/set types:

collections::HashMap and collections::HashSet, requiring the keys to implement Eq and Hash
collections::TrieMap and collections::TrieSet, requiring the keys to be uint
collections::TreeMap and collections::TreeSet, requiring the keys to implement Ord

Oh! It's collections::HashMap, not collections::hashmap::HashMap. Compile:

main.rs:14:19: 14:26 error: unresolved name `HashMap`.
main.rs:14     let mut map = HashMap<box str; uint>::new();
                             ^~~~~~~
main.rs:14:31: 14:34 error: unresolved name `str`.
main.rs:14     let mut map = HashMap<box str; uint>::new();
                                         ^~~
main.rs:14:36: 14:40 error: unresolved name `uint`. Did you mean `map`?
main.rs:14     let mut map = HashMap<box str; uint>::new();
                                              ^~~~
main.rs:14:41: 14:46 error: unresolved name `new`. Did you mean `map`?
main.rs:14     let mut map = HashMap<box str; uint>::new();

Damn. Weirdly, it's not actually complaining about the use statement. Is there even an error if use refers to a nonexistent thing?

main.rs:3:5: 3:23 error: unresolved import `std::butt::Octopus`. Could not find `butt` in `std`.
main.rs:3 use std::butt::Octopus;
              ^~~~~~~~~~~~~~~~~~

OK, so the error reporting is working. So it is importing HashMap, but it's still an unresolved name when I try and use it. OK, back to the docs. So, HashMap might be undocumented, but its usage can't be too different from TreeMap. Let's have a look at the TreeMap docs.

This is interesting; these docs refer to TreeMap<K, V> with a comma between K and V. Was I wrong about that semicolon all along? Now that I go back and look, all the examples used commas too. Alright, so, change it back to a comma:

HashMap<box str, uint>
    main.rs:14:34: 14:35 error: expected `;` but found `,`
    main.rs:14     let mut map = HashMap<box str, uint>::new();

So let's assume this is just a misleading error message. Let's try:

HashMap<box(str), uint>
    main.rs:14:35: 14:36 error: unexpected token: `,`
    main.rs:14     let mut map = HashMap<box(str), uint>::new();

HashMap<box<str>, uint>
    main.rs:14:30: 14:31 error: unexpected token: `<`
    main.rs:14     let mut map = HashMap<box<str>, uint>::new();

HashMap<str, uint>
    main.rs:14:30: 14:31 error: expected `;` but found `,`
    main.rs:14     let mut map = HashMap<str, uint>::new();

HashMap<&str, uint>
    main.rs:14:31: 14:32 error: expected `;` but found `,`
    main.rs:14     let mut map = HashMap<&str, uint>::new();

HashMap<String, uint>
    main.rs:14:33: 14:34 error: expected `;` but found `,`
    main.rs:14     let mut map = HashMap<String, uint>::new();

HashMap<uint, uint>
    main.rs:14:31: 14:32 error: expected `;` but found `,`
    main.rs:14     let mut map = HashMap<uint, uint>::new();

TreeMap<uint, uint>
    main.rs:14:31: 14:32 error: expected `;` but found `,`
    main.rs:14     let mut map = TreeMap<uint, uint>::new();

OK, I'm not getting anywhere here. Trawl the docs some more. Now I find:

    let mut map = TreeMap::new();

    map.insert(2i, "bar");
    map.insert(1i, "foo");
    map.insert(3i, "quux");

That doesn't have any type parameters at all! How can that possibly work? Indeed, it doesn't:

main.rs:14:19: 14:31 error: cannot determine a type for this bounded type parameter: unconstrained type
main.rs:14     let mut map = TreeMap::new();
                             ^~~~~~~~~~~~

Huh. Must be a 0.13.0 feature. Unless... it manages to determine the type from those insert() calls afterwards? That would be clever...

let mut map = TreeMap::new();
map.insert(0i, 0i);

And it compiles! Can I change it to a HashMap now? Yes! OK, level up. I now have a working understaning of maps. As far as I can tell:

This probably isn't right (I'm sure empty maps must be supported somehow), but it will do for now.

Can we change our HashMap back to use strings? Actually, why waste time trying to do that; we never even wanted strings in the first place. We want a mapping of co-ordinate pairs to lists of objects. IIRC an Array in Rust is a fixed-length homogenous datatype with random access, so a 2-array seems like a good fit for storing a co-ordinate pair. Google “Rust typedef”. OK, I think we need:

type Coord = [int, ..2];

Nice! Let's also change our HashMap. We'll use ints for our objects for now until we decide how we want to represent them.

map.insert([1i, 1i], 0i);

Compile:

main.rs:17:5: 17:29 error: failed to find an implementation of trait core::cmp::Eq for [int, .. 2]
main.rs:17     map.insert([1i, 1i], 0i);
               ^~~~~~~~~~~~~~~~~~~~~~~~

Ah, OK. So I think this means Rust has no built-in way of determining if two int-arrays are equal. A little Googling uncovers this StackOverflow question.

Let's adapt that code:

type Coord = [int, ..2];

impl Eq for Coord {
    fn eq(&self, other: &Coord) -> bool {
        self.iter().zip(other.iter()).all(|(a,b)| a == b);
    }
}

Compile:

main.rs:7:5: 9:6 error: method `eq` is not a member of trait `Eq`
main.rs:7     fn eq(&self, other: &Coord) -> bool {
main.rs:8         self.iter().zip(other.iter()).all(|(a,b)| a == b);
main.rs:9     }

Ah, they might have changed Eq. Look it up in the 0.11.0 docs... OK, so no methods. They must all be in PartialEq:

fn eq(&self, other: &Self) -> bool
fn ne(&self, other: &Self) -> bool

Right. So, this would suggest that eq is a member of trait Eq. Do I just need to import Eq? Nope. OK, Google time.

Alright, no help. Perhaps if I use a struct instead of an array?

struct Coord {
    x: int,
    y: int
}

[...]

map.insert(Coord{x: 0, y: 0}, 0i);

Compile:

main.rs:11:5: 13:6 error: method `eq` is not a member of trait `Eq`
main.rs:11     fn eq(&self, other: &Coord) -> bool {
main.rs:12         return true;
main.rs:13     }

Alright, I'm out of ideas.

So, logged on to the IRC and got some great help from eddyb, mindtree and Quxxy on there.

First lesson: always use the nightlies! Go home, homebrew, you're drunk.

Second lesson: The eq operator is in PartialEq, so I need to implement that before implementing Eq.

Third lesson: I had the syntax wrong for type parameters. It's

HashMap::<K, V>::new()

Not

HashMap<K, V>::new()

Too much C++ in this brain of mine.

So, it seems fixed-length arrays are getting an Eq implementation very soon, but not quite yet. So, I'm going to use a struct to keep things simple. eddyb explains ‘deriving’ to me, so I end up with:

#[deriving(PartialEq, Eq, Hash)]
struct Coord { x: int, y: int }

fn main() {
    let mut map = HashMap::new();
    map.insert(Coord{x:0, y:0}, 0i);
}

Deriving has a built-in list of traits it can derive for simple structs. You don't need to import these traits; Deriving knows where they are.

I should mention I'm also using the latest Rust now, which is 0.13.0.

That Coord{x:0, y:0} syntax is a bit heavy. Let's make a function to shorten it a bit:

fn xy(x: int, y: int) {
    Coord{x:x, y:y}
}

fn main() {
    let mut map = HashMap::new();
    map.insert(xy(0i, 0i), 0i);
}

In fact, we can probably get rid of those ‘i’s now:

map.insert(xy(0, 0), 0);

Hmm, nope. Ah yes, because now we aren't saying what the value type of the HashMap is. Try again:

fn main() {
    let mut map = HashMap::<Coord, int>::new();
    map.insert(xy(0, 0), 0);
}

Success! We can clean this up further with a typedef:

type LevelMap = HashMap<Coord, int>;

fn main() {
    let mut map = LevelMap::new();
}

Ah, no, we can't. After some IRCing, it turns out type isn't quite like C's typedef — it doesn't let you call static methods through the new type. But, we can write our own constructor as a free function:

type LevelMap = HashMap<Coord, int>;

fn mklevel() -> LevelMap {
    HashMap::<Coord, int>::new()
}

fn main() {
    let mut map = mklevel();
}

As far as I can tell, this is considered a wart. A bit more cleaning up, and now we have:

use std::collections::HashMap;

#[deriving(PartialEq, Eq, Hash)]
struct Coord { x: int, y: int }

type LevelMap = HashMap<Coord, int>;

fn mklevel() -> LevelMap {
    HashMap::<Coord, int>::new()
}

fn xy(x: int, y: int) -> Coord {
    Coord{x:x, y:y}
}

fn main() {
    let mut map = mklevel();
    map.insert(xy(0, 0), 0);
}

Compiled successfully! More to come.

Part 2

2015-03-12 — Using Rust 0.0.13

Right, so we said we'd use immutable types, so let's confine the mutability of map to the mklevel function:

fn mklevel() -> LevelMap {
    let mut map = HashMap::<Coord, int>::new();
    map.insert(xy(0, 0), 0);
    map
}

fn main() {
    let map = mklevel();
}

Seems to work. The compiler just gives a warning about not using our map variable, which is fair enough.

I think now it's time to re-introduce our draw function, but this time have it actually draw what's in the map. We don't have proper objects yet, but we can still draw, say, ‘#’ where there is an object and ‘.’ where there isn't. First try:

fn cell_glyph(map: LevelMap, coord: Coord) -> Char {
    match map.find(coord) {
        Some(_) => '#',
        None => '.'
    }
}

fn draw(map: LevelMap) {
    for x in range(0i, 10i) {
        for y in range(0i, 10i) {
            let coord = xy(x, y);
            let glyph = cell_glyph(map, coord);
            print!("{}", glyph);
        }
        print!("\n");
    }
}

The docs were pretty clear about retrieving map items and interpolating values into print! strings with {}. I'm not too sure about that Char, though, and my '#' character literal syntax is a total guess. Let's compile:

main.rs:18:47: 18:51 error: explicit lifetime bound required
main.rs:18 fn cell_glyph(map: LevelMap, coord: Coord) -> Char {
                                                         ^~~~

OK, so it looks like the time has come to learn about Rust's famous “lifetimes”. Though I'm not sure why a simple character needs lifetime management. Wait, I think I made a mistake.

fn cell_glyph(map: LevelMap, coord: Coord) -> char {

Lowercase char. That's better. I can put off learning about lifetimes after all. Next error:

main.rs:19:20: 19:25 error: mismatched types: expected `&Coord`, found `Coord` (expected &-ptr, found struct Coord)
main.rs:19     match map.find(coord) {
                              ^~~~~

Well, if “&-pointers” are anything like references in C++, I guess we could try:

fn cell_glyph(map: LevelMap, coord: &Coord) -> char {

Compile:

main.rs:29:41: 29:46 error: mismatched types: expected `&Coord`, found `Coord` (expected &-ptr, found struct Coord)
main.rs:29             let glyph = cell_glyph(map, coord);
                                                   ^~~~~
error: aborting due to previous error

Promising! Let's sprinkle a few more &s...

fn cell_glyph(map: &LevelMap, coord: &Coord) -> char {
    match map.find(coord) {
        Some(_) => '#',
        None => '.'
    }
}

fn draw(map: LevelMap) {
    for x in range(0i, 10i) {
        for y in range(0i, 10i) {
            let coord = xy(x, y);
            let glyph = cell_glyph(&map, &coord);
            print!("{}", glyph);
        }
        print!("\n");
    }
}

Alright! This compiles, with just a couple of warnings about the fact that we're not calling any of these functions. Let's do so:

fn main() {
    let map = mklevel();
    draw(map);
}

Still compiles. It's finally time... runtime!

$ rustc main.rs && ./main
#.........
..........
..........
..........
..........
..........
..........
..........
..........
..........

Beautiful!

So, &-references seem to work a lot like C++ references, which will be easy to remember, since the notation is similar. I'm going to think of these as references pointing back up the stack to stack-allocated objects in the calling function. I bet you can't return one:

fn test() -> &Coord {
    let coord = xy(0, 0);
    &coord
}

main.rs:36:14: 36:20 error: missing lifetime specifier [E0106]
main.rs:36 fn test() -> &Coord {
                        ^~~~~~
main.rs:36:14: 36:20 note: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
main.rs:36 fn test() -> &Coord {
                        ^~~~~~
main.rs:36:14: 36:20 note: consider giving it a 'static lifetime
main.rs:36 fn test() -> &Coord {
                        ^~~~~~

OK, that's reassuring. I think I'm right.

Now, it's about time we added a player. Right now the only kind of object is a wall, so we're going to need some way to distinguish between players and walls. We need a game object system.

There are plenty of ways to structure game code, but at the moment I'm really into entity-component systems. If you want to read about what these are, this is a nice introduction.

This feels like it will be a pretty good match for Rust's static type system, and it'll be a good time to learn about Rust modules too, since each system can have its own module. Let's start with the position system:

mod position {

    struct Position {
        coord: ::Coord
    }

    struct State {
        objects: Vec<Position>
    }

}

Pretty straightforward. ::Coord refers to Coord from the global namespace and was the first thing I tried after Coord on its own didn't work. Vec is, I believe, a dynamically-allocated array, similar to C++'s std::vector.

Let's also define our main game State object, which will hold the state of the entire game. This will be composed of the state of each system, so for now, it will only include the position system's state:

struct State {
    position: position::State
}

Oops, position::State needs to be public.

pub struct State {
    objects: Vec<Position>
}

That's better. Now let's add the graphics system:

mod graphics {
    struct Sprite {
        id: uint,
        glyph: char
    }

    pub struct State {
        objects: Vec<Sprite>
    }
}

struct State {
    position: position::State,
    graphics: graphics::State
}

These systems are looking very similar, and they're probably going to need some of the same functions, too, for adding, changing and deleting their objects. There's probably a good way to factor this common stuff out, but let's leave it as is for now.

Now, let's write a function to create a new, empty game state:

fn mkstate() -> State {
    State {}
}

Hmm, nope. How do you default-initialize things? Google to the rescue: we need to use std::default::Default, and we also need to put #[deriving(Default)] on our State struct as well as both of the structs it contains. Then we can do:

fn mkstate() -> State {
    Default::default()
}

Alright. Let's also add an object to our starting state, with a position and a graphic:

fn mkstate() -> State {
    let mut state: State = Default::default();
    let id = 42u;
    let coord = Coord { x: 1, y: 1 };
    let glyph = '@';
    state.position.objects.push(position::Position {id: id, coord: coord});
    state.graphics.objects.push(graphics::Sprite {id: id, glyph: glyph});
    state
}

In order for this to compile, I had to make pretty much everything pub:

mod graphics {
    pub struct Sprite {
        pub id: uint,
        pub glyph: char
    }

    [... and so on ...]

That feels wrong... I wonder if public/private are relative to the module rather than the struct? That would make sense. Then a cleaner way would be to write a constructor for Sprite inside the graphics module. But, I'd want to set all the fields of the new Sprite anyway, so that would just be more code for no benefit. Maybe making all the things pub is idiomatic after all?

In order to draw our objects, the first thing we'll need is a function to find the object at a particular position. This belongs in the position module, since it deals with positions and nothing else:

pub fn id_at(state: &::State, coord: &::Coord) {
}

We need to iterate through all the position-objects in the state and return the first one matching the given Coord. (Yes, this is less efficient than our hash map from earlier — let's get our object system sorted first, then we can fix that.)

There's probably some sort of find method on Vec. Let's consult the docs. Vec page.

Ctr-F find.
Ctrl-F where.
Ctrl-F fn where.
Ctrl-F first

No luck. Google for rust map reduce because these will probably be easier to find than find. A Reddit post leads me here, which in turn leads me to iterators and collections.

Flip a coin. Collections it is. Scroll down until a section catches my eye. It is called iterators. Iterators are lazily-evaluated. Collections all provide iter, iter_mut and into_iter. I can probably guess what these do.

OK! I guessed wrong. iter_mut gives you mutable references to the collection and into_iter turns the collection itself into an iterator. I guess that means the original collection disappears? It must do, otherwise what would be the difference between iter and into_iter.

A lot of the examples on this page use .iter(), so maybe the methods I'm looking for are on the iterator object itself. Let's try that “iterator” link from before. OK, lots of iterator traits. Pick one at random. Actually let's pick Iterator.

Whoa, okay, here are all the methods. I see map, reduce, filter. Aha, here's find:

fn find(&mut self, predicate: |&A| -> bool) -> Option<A>

So we need a predicate function. Quick look through the manual to see how lambda functions work. OK, simple enough. Let's see if we can figure this out:

pub fn id_at(state: &::State, coord: &::Coord) -> Option<uint> {
    let x = state.position.objects.iter().find(|item| item.pos == coord);
    match x {
        Some(item) => Some(item.id)
        None => None
    }
}

OK, two errors. First one:

main.rs:22:73: 22:78 error: mismatched types: expected `Coord`, found `&Coord` (expected struct Coord, found &-ptr)
main.rs:22         let x = state.position.objects.iter().find(|item| item.coord == coord);
                                                                                   ^~~~~

So that &-pointer doesn't dereference automatically. I wonder how you dereference one. Google... ah, it's ‘*’, same as C. Let's fix our predicate:

|item| item.coord == *coord

That solves that. Now onto that other error:

main.rs:23:9: 26:10 error: match arms have incompatible types: expected `uint`, found `core::option::Option<_>` (expected uint, found enum core::option::Option)
main.rs:23         match x {
main.rs:24             Some(item) => { item.id }
main.rs:25             None => None
main.rs:26         }
main.rs:25:21: 25:25 note: match arm with an incompatible type
main.rs:25             None => None
                               ^~~~

Why does Rust expect uint here? I've declared this function to return Option<uint>. Actually, this whole match statement is kind of bulky and I had planned to get it working with match and then see if Rust had anything like Scala's Map for Options. Since match is giving us grief, let's just do that now. Guessing:

pub fn id_at(state: &::State, coord: &::Coord) -> Option<uint> {
    let x = state.position.objects.iter().find(|item| item.coord == *coord);
    x.map(|item| item.id)
}

Success! Let's compact it:

pub fn id_at(state: &::State, coord: &::Coord) -> Option<uint> {
    state.position.objects.iter()
        .find(|item| item.coord == *coord)
        .map(|item| item.id)
}

Still works.

That's actually enough to adapt our cell_glyph function now:

fn cell_glyph(state: &State, coord: &Coord) -> char {
    match position::id_at(state, coord) {
        Some(_) => '?',
        None => '.'
    }
}

$ rustc main.rs && ./main
..........
.?........
..........
..........
..........
..........
..........
..........
..........
..........

Aw yiss. There's our default object at [1, 1]. Let's see if we can kick our graphics system into action. We'll need a function to get an object's graphic, inside mod graphics:

pub fn glyph_for(state: &::State, id: uint) -> Option<char> {
    state.graphics.objects.iter()
        .find(|item| item.id == id)
        .map(|item| item.glyph)
}

Actually, what use is an Option<char>? The graphics system should be more assertive. Looking through the list of Option methods I see unwrap_or:

pub fn glyph_for(state: &::State, id: uint) -> char {
    state.graphics.objects.iter()
        .find(|item| item.id == id)
        .map(|item| item.glyph)
        .unwrap_or('?')
}

There's also map_or which would make this even shorter, but IMHO less readable.

I notice the doc page for Option doesn't have a summary of methods at the start like the Iterator page does; you have to scroll past the definition of each Option method in turn. I prefer the Iterator page.

Anyway, let's use glyph_for in the draw code:

fn cell_glyph(state: &State, coord: &Coord) -> char {
    match position::id_at(state, coord) {
        Some(id) => graphics::glyph_for(state, id),
        None => '.'
    }

And let's add another object to prove we can have objects with different glyphs:

fn mkstate() -> State {
    let mut state: State = Default::default();

    fn mktest(id: uint, coord: &Coord, glyph: char) {
        state.position.objects.push(position::Position {id: id, coord: coord});
        state.graphics.objects.push(graphics::Sprite {id: id, glyph: glyph});
    }

    mktest(42, xy(1, 1), '@');
    mktest(57, xy(8, 8), '$');

    state
}
main.rs:58:9: 58:14 error: can't capture dynamic environment in a fn item; use the || { ... } closure form instead
main.rs:58         state.position.objects.push(position::Position {id: id, coord: coord});
                   ^~~~~

Huh. Try again:

fn mkstate() -> State {
    let mut state: State = Default::default();

    let mktest = |id: uint, coord: Coord, glyph: char| {
        state.position.objects.push(position::Position {id: id, coord: coord});
        state.graphics.objects.push(graphics::Sprite {id: id, glyph: glyph});
    };

    mktest(42, xy(1, 1), '@');
    mktest(57, xy(8, 8), '$');

    state
}

main.rs:65:5: 65:10 error: cannot move out of `state` because it is borrowed
main.rs:65     state
               ^~~~~
main.rs:57:18: 60:6 note: borrow of `state` occurs here
main.rs:57     let mktest = |id: uint, coord: Coord, glyph: char| {
main.rs:58         state.position.objects.push(position::Position {id: id, coord: coord});
main.rs:59         state.graphics.objects.push(graphics::Sprite {id: id, glyph: glyph});
main.rs:60     };

Huh. Alright, I don't know what “moving out of” a variable is, so I'll just use a global function. Look up how to make references mutable — it's &mut State — and:

fn mktest(state: &mut State, id: uint, coord: Coord, glyph: char) {
    state.position.objects.push(position::Position {id: id, coord: coord});
    state.graphics.objects.push(graphics::Sprite {id: id, glyph: glyph});
}

fn mkstate() -> State {
    let mut state: State = Default::default();

    mktest(&mut state, 42, xy(1, 1), '@');
    mktest(&mut state, 57, xy(8, 8), '$');

    state
}

$ rustc main.rs && ./main
..........
.@........
..........
..........
..........
..........
..........
..........
........$.
..........

There it is.

Part 3

2015-03-12 — Using Rust 0.0.13

This is I think an appropriate juncture at which to start accepting some user input. Looks like I need to

use std::io::stdio::stdin;

and then we can

fn get_input() -> Option<char> {
    let x = stdin.read_char();

read_char gets us an IOResult<char> which can either be Ok or Err, so:

    match x {
        Ok(v) => Some(v),
        Err(_) => None
    }
}

This doesn't compile. Ah, OK, stdin is a function.

let x = stdin().read_char();

OK, that compiles. Pretty painless. Now let's set up a loop in which we repeatedly get user input, update the state, and draw the state. Actually, since Rust has a bit of a pure-functional thing going on, let's use recursion instead of a loop:

fn main() {
    let state = mkstate();
    process(&state);
}

fn process(state: &State) {
    match get_input() {
        Some(ch) => {
            let state1 = update(state, ch);
            draw(&state1);
            process(&state1);
        },
        None => { return; }
    }
}

Our update function will just return the state unchanged for now, but the plan is to have it return a new, updated state.

fn update(state: &State, input: char) -> State {
    state.clone()
}

At this point it turns out that, unlike in C++, the Rust compiler can't figure out how to clone your structs unless you add #[deriving(Clone)] to them. Let's add that to State. We'll also need to add it to all the structs it contains, so that's all of our graphics and position structs too.

It'll also be necessary to clear the screen each time we draw:

fn clear_screen() {
    print!("\x1b[2J\n");
}

Alright! This all compiles and runs. I feel like I'm starting to get the hang of this now. One problem, though, is that read_char doesn't read a character immediately; you have to input a whole line of text before your program starts seeing any of the characters, which isn't what we want. It doesn't look like Rust has a built-in way of changing this, and I don't want to get into C FFIs just yet, so let's just cheat and write a wrapper script for our program:

stty cbreak
./main
stty sane

So, it'd be nice if all this player input actually controlled the player. We'll need a player-system in the long run, but let's just get something working for now: we'll find the player by its glyph and move in whichever direction the player specified.

The first thing we'll need is a mapping of keys to directions. Pattern matching is pretty neat so let's try using that:

fn key_dir(input: char) -> Coord {
    match input {
        'h' => xy(-1, 0),
        'j' => xy(0, 1),
        'k' => xy(0, -1),
        'l' => xy(1, 0),
        _ => xy(0, 0)
    }
}

All good. A function to add two Coords:

fn vadd(a: Coord, b: Coord) -> Coord {
    xy(a.x + b.x, a.y + b.y)
}

One to find our player, borrowing heavily from earlier:

fn find_player(state: &::State) -> Option<uint> {
    state.graphics.objects.iter()
        .find(|item| item.glyph == '@')
        .map(|item| item.id)
}

All this is compiling without a hitch. Okay, now for the tricky part. We need to have our update function take the state, find the player, and return a new state in which the player has moved. I remember seeing about something called “functional update syntax” in the docs — let's look that up.

Google “Rust functional update syntax”
Google “Rust record update”

Hmm, no official doc links. The best result seems to be this mailing list post, suggesting improvements to the record update syntax. It does mention the “current syntax” as it was on Mon Aug 6 13:49:21 PDT 2012:

struct A {
  a : int;
  b : int;
}

let some_a = A { a : 10, b : 10 };
let other_a = A { a : 0 with some_a };

I guess it might still be the same?

main.rs:128:38: 128:42 error: expected one of `,`, `}`, found `with`
main.rs:128     State { position: state.position with state }
                                                 ^~~~

No such luck! I'm sure I've seen that doc page.

Google “rust struct update syntax”

Maybe it's on the page about structs? Okay. The Rust Reference. Statements and Expressions. Expression Statements. Expressions. Literal Expressions. Structure Expressions! Alright, here we are, §7.2.4:

struct_expr : expr_path '{' ident ':' expr
                      [ ',' ident ':' expr ] *
                      [ ".." expr ] '}' |
              expr_path '(' expr
                      [ ',' expr ] * ')' |
              expr_path ;

That's it. I remember now.

fn update(state: &State, input: char) -> State {
    State { position: state.position, ..state }
}

main.rs:128:41: 128:46 error: mismatched types: expected `State`, found `&State` (expected struct State, found &-ptr)
main.rs:128     State { position: state.position, ..state }
                                                    ^~~~~

Oh, right. Dereference.

fn update(state: &State, input: char) -> State {
    State { position: state.position, ..*state }
}

main.rs:128:41: 128:47 error: cannot move out of dereference of `&`-pointer
main.rs:128     State { position: state.position, ..*state }
                                                    ^~~~~~

Aargh, there's that “move out of” language again. I guess it's time to actually find out what that means.

Google “rust cannot move out of dereference”

Nothing super relevant, but it seems “moving out of” a variable is when a reference “changes hands” and is no longer accessible from its old location? This reminds me of that bit about iter and to_iter. Doesn't seem relevant here though. Maybe if I copy?

fn update(state: &State, input: char) -> State {
    State { position: state.position, ..*state.clone() }
}

main.rs:128:41: 128:55 error: type `State` cannot be dereferenced
main.rs:128     State { position: state.position, ..*state.clone() }
                                                    ^~~~~~~~~~~~~~

Oh. I'm not really clear on when you're supposed to dereference pointers.

fn update(state: &State, input: char) -> State {
    State { position: state.position, ..state.clone() }
}


main.rs:128:23: 128:28 error: cannot move out of dereference of `&`-pointer
main.rs:128     State { position: state.position, ..state.clone() }
                                  ^~~~~

Oh, right, I have to copy this too.

fn update(state: &State, input: char) -> State {
    State { position: state.position.clone(), ..state.clone() }
}

Alright. Seems like it must be a bit of a pain to type clone all over the place, but maybe I'm doing it wrong. This compiles, though, so let's press on:

fn update(state: &State, input: char) -> State {
    let player = find_player(state);
    match find_player(state) {
        Some(player) => {
            let dir = key_dir(input);
            State { position: state.position.clone(), ..state.clone() }
        }
        None => state
    }
}

main.rs:129:5: 135:6 error: match arms have incompatible types: expected `State`, found `&State` (expected struct State, found &-ptr)
main.rs:129     match find_player(state) {
main.rs:130         Some(player) => {
main.rs:131             let dir = key_dir(input);
main.rs:132             State { position: state.position.clone(), ..state.clone() }
main.rs:133         }
main.rs:134         None => state
            ...
main.rs:134:17: 134:22 note: match arm with an incompatible type
main.rs:134         None => state
                            ^~~~~

Huh, we can't return the original State if nothing changes, because that's a borrowed reference, and the function needs to return a... er, a normal reference. And we can't just return *state because that just brings us back to our old friend “cannot move out of dereference”. Is copying the answer?

    None => state.clone()

Yes!, probably. Okay, so now we need to move the player. We want the same graphics-state, and a similar position-state except for its list of objects, where the object with the player's ID needs to have the player's new position. Does Vec have a functional-style replace? Oh, no, these things are on Iterator, aren't they? Okay, scanning the handy list of methods on the Iterator page... no replace. So, what can we use?

There's find which will give us the player element itself — no use, we already have that.

There's position which would get us the player's index in the list of objects. But then we'd need a method on Vec to replace that index. Is there one? Vec page, Ctrl-F “replace”. Damn.

Okay, I guess let's just map over all the objects and modify the player if we find it. This is super-inefficient since it'll keep going after it finds the player, but never mind.

fn update(state: &State, input: char) -> State {
    let player = find_player(state);
    match find_player(state) {
        Some(player) => {
            let dir = key_dir(input);
            let old_position = position::position_of(state, player).unwrap();
            let new_position = vadd(old_position, dir);
            State { 
                position: position::State {
                    objects: state.position.objects.iter().map(
                        |object|
                        if (object.id == player)  {
                            position::Position { coord: new_position, ..object.clone() }
                        }
                        else {
                            object.clone()
                        }
                    ),
                    ..state.position.clone()
                },
                ..state.clone() 
            }
        }
        None => state.clone()
    }
}

Pretty nasty. I wonder if there's a nicer alternative to this record update syntax. Anyway, can't worry about that just yet because this thing emits an error message worthy of GCC:

main.rs:142:30: 150:22 error: mismatched types: expected `collections::vec::Vec<position::Position>`, found `core::iter::Map<'_, &position::Position, position::Position, core::slice::Items<'_, position::Position>>` (expected struct collections::vec::Vec, found struct core::iter::Map)
main.rs:142                     objects: state.position.objects.iter().map(
main.rs:143                         |object|
main.rs:144                         if (object.id == player)  {
main.rs:145                             position::Position { coord: new_position, ..object.clone() }
main.rs:146                         }
main.rs:147                         else {

I think it's trying to tell me to turn that iterator returned by map back into a Vec. But how?

Part 4

2015-03-12 — Using Rust 1.0.0

Picking up this project again after a bit of a break, the first thing I did was to update Rust to the latest nightly build, which has reached 1.0.0! This sounds like a far cry from the paltry 0.13.0 I was using before. So, let's compile and see if anything breaks:

$ rustc main.rs && ./main

main.rs:6:1: 6:40 error: `deriving` has been renamed to `derive`
main.rs:6 #[deriving(PartialEq, Eq, Hash, Clone)]
          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

(... and 5 more similar errors ...)

OK, that doesn't sound too bad. Let's change those, and:

main.rs:7:19: 7:22 warning: the `int` type is deprecated; use `isize` or a fixed-sized integer
main.rs:7 struct Coord { x: int, y: int }
                            ^~~
main.rs:7:22: 7:22 help: add #![feature(int_uint)] to the crate attributes to silence this warning

(... lots more of these ...)

Right! So, what is isize? The docs say it is for “pointer-sized integers”. Okay, I guess? Let's just change all ints to isize and our uints to usize. And it seems we also have to change the i suffixes on our numeric literals to isize. That seems very bulky, but it turns out we can just remove them altogether and nothing explodes. What's next?

$ rustc main.rs && ./main

main.rs:141:30: 149:22 error: mismatched types:
 expected `collections::vec::Vec<position::Position>`,
    found `core::iter::Map<core::slice::Iter<'_, position::Position>, [closure main.rs:142:25: 148:26]>`
(expected struct `collections::vec::Vec`,
    found struct `core::iter::Map`) [E0308]
main.rs:141                     objects: state.position.objects.iter().map(
main.rs:142                         |object|
main.rs:143                         if (object.id == player)  {
main.rs:144                             position::Position { coord: new_position, ..object.clone() }
main.rs:145                         }
main.rs:146                         else {
            ...

That does not look fun. Alright, so we've got a Map where we need a Vector. Actually, it just looks like Rust has done something similar to what Python did when they made map() return a weird map-object instead of a list and it became super annoying to use.

Google tells me I can convert any Iterator into a Vector using collect. It works! Onwards...

$ rustc main.rs && ./main

main.rs:164:21: 164:32 error: type `std::io::stdio::Stdin` does not implement any method in scope named `read_char`
main.rs:164     let x = stdin().read_char();
                                ^~~~~~~~~~~

OK, it seems I now want stdin_raw. And there's no read_char — I can choose between read_byte and read_u8. Docs, what is the difference?

fn read_byte(&mut self) -> IoResult<u8>
    Reads a single byte. Returns Err on EOF.

fn read_u8(&mut self) -> IoResult<u8>
    Read a u8.
    u8s are 1 byte.

Okay, so on EOF, read_byte returns Err whereas read_u8 does... something. Let's just use read_byte since we know more about it.

main.rs:164:25: 164:34 error: type `std::io::stdio::StdinRaw` does not implement any method in scope named `read_u8`
main.rs:164     let x = stdin_raw().read_u8();
                                    ^~~~~~~~~

What? The docs say stdin_raw should be a StdinReader not a StdinRaw. Now I notice they also say “don't use io, use old_io”. Let's change it all back.

use std::old_io::stdio::stdin;
...
let x = stdin().read_char();

That sorted it. Oh god there's more.

main.rs:32:25: 32:29 error: cannot move out of borrowed content
main.rs:32             .map(|item| item.coord)
                                   ^~~~
main.rs:144:57: 144:69 error: cannot move out of captured outer variable in an `FnMut` closure
main.rs:144                             position::Position { coord: new_position, ..object.clone() }
                                                                    ^~~~~~~~~~~~

I feel like this can be solved by sprinkling clone() dust. Let's try:

pub fn position_of(state: &::State, id: usize) -> Option<::Coord> {
    state.position.objects.iter()
        .find(|item| item.id == id)
        .map(|item| item.coord.clone())
}

Success. Now we only have warnings:

main.rs:3:5: 3:30 warning: use of unstable library feature 'old_io'
main.rs:3 use std::old_io::stdio::stdin;
              ^~~~~~~~~~~~~~~~~~~~~~~~~
main.rs:3:30: 3:30 help: add #![feature(old_io)] to the crate attributes to silence this warning

Funny, the docs say it's the new IO that's unstable. Google “crate attributes” and it seems they just go at the top of the file. Sorted.

main.rs:111:14: 111:19 warning: use of unstable library feature 'core': will be replaced by range notation
main.rs:111     for x in range(0, 10) {
                         ^~~~~

Range notation, eh? Sounds nice. So instead of doing

for x in range(0, 10) {

we do

for x in 0..10 {

And there are no more errors! Reassuringly, once it compiles, the program runs the same as before.

That's about as much Rust as I want to do today. I've received some great feedback on part 1 so next time I'll try implementing some of the improvements that have been suggested to me. More to come.

Part 5

2016-03-19 — Using Rust 1.7.0, 1.9.0

It's been a year since we left off, and I feel like giving Rust another go. So, let's upgrade to rustc 1.7 and see if our code still compiles:

main.rs:1:1: 1:20 error: #[feature] may not be used on the stable release channel
main.rs:1 #![feature(old_io)]
          ^~~~~~~~~~~~~~~~~~~

OK, so the io library seems to have changed quite a bit. We switch off old_io, and our problem moves to get_input:

main.rs:166:21: 166:32 error: no method named `read_char` found for type `std::io::stdio::Stdin` in the current scope
main.rs:166     let x = stdin().read_char();
                                ^~~~~~~~~~~

So, what's the new way to read a char? Check the docs. We can read, read_string read_to_end, but not read_char. If we read we read into a buffer. We also have bytes and chars which “transform this Read instance to an Iterator over chars [or bytes.]”. Let's try chars, and call next on the result to get the next input character.

I notice next on Chars returns a rather hairy-looking Option<Result<char, CharsError>>. It looks like the Option will be None when the stream is finished, and the Result will be Err if some I/O error occurs. That seems fair enough. Our get_input now reads:

fn get_input() -> Option<char> {
    let mut x = stdin().chars();
    match x.next() {
        Some(v) => v.ok(),
        None => None
    }
}

The ok() converts the Result into an Option. We'll just bail on IO errors. The correct import statement has also changed and now seems to be:

use std::io::{stdin, Read};

Great, what's next?

main.rs:15:20: 15:27 error: private type in public interface [E0446]
main.rs:15         pub coord: ::Coord
                              ^~~~~~~
main.rs:15:20: 15:27 help: run `rustc --explain E0446` to see a detailed explanation

The problem seems to be that our top-level structs now need to be public in order to be seen from our modules.

This new rustc --explain is very nice and helps me solve the issue right away. OK, in this case it was pretty self-explanatory anyway. But I can see this being very useful in the future.

Just one error left:

main.rs:166:25: 166:32 error: use of unstable library feature 'io': the semantics of a partial read/write of where errors happen is currently unclear and may change (see issue #27802)
main.rs:166     let mut x = stdin().chars();
                                    ^~~~~~~

OK, I'll admit that this time, I installed the stable version of Rust, despite warnings last time to only use the nightlies. Hey, I thought maybe things had changed. It's been a year.

But, since I'm on stable Rust, I can't use io, because it's still unstable. And I can't use feature(old_io) either. So just what am I supposed to use? old_io without feature? Nope, doesn't work.

Alright, back to the nightlies. Add #![feature(io)] to the top and we're good to go. Lesson learned. When the Rust people say “always use the nightlies”, they mean ALWAYS use the nightlies.

OK, everything now compiles and works as before. We've run fast enough to keep still. Now, before implementing anything new, let's try to clean up our code a bit.

Firstly, at HN user sanderjd's suggestion, let's change our

#[derive(PartialEq, Eq, Hash, Clone)]
pub struct Coord { x: isize, y: isize }

to

#[derive(PartialEq, Eq, Hash, Clone)]
pub struct Coord(isize, isize);

Our adding function now becomes:

fn vadd(a: Coord, b: Coord) -> Coord {
    Coord(a.0 + b.0, a.1 + b.1)
}

And we don't need xy().

Nice! I already prefer numeric rather than x/y syntax for vectors, just because it generalizes better. And deleting code is my favourite thing.

We can also remove our HashMap since we never actually started using it, and optimization seems like a long way off.

Now, aside from the nasty update function, which I'm sure can be done better somehow, this code would almost be neat, if it weren't for the #[derive] warts everywhere. Can we remove any of them?

Experimentation shows that most are still necessary, but Eq can disappear. Oh well, that's something.

Let's take a look at that update function. Currently we have:

fn update(state: &State, input: char) -> State {
    match find_player(state) {
        Some(player) => {
            let dir = key_dir(input);
            let old_position = position::position_of(state, player).unwrap();
            let new_position = vadd(old_position, dir);
            State {
                position: position::State {
                    objects: state.position.objects.iter().map(
                        |object|
                        if object.id == player {
                            position::Position {
                                coord: new_position.clone(),
                                 ..object.clone()
                            }
                        }
                        else {
                            object.clone()
                        }
                    ).collect(),
                    ..state.position.clone()
                },
                ..state.clone() 
            }
        }
        None => state.clone()
    }
}

Syntax aside, part of the issue is that it's doing a couple of different things, so let's split it apart first:

fn update(state: &State, input: char) -> State {
    match find_player(state) {
        Some(player) => {
            let dir = key_dir(input);
            let old_position = position::position_of(state, player).unwrap();
            let new_position = vadd(old_position, dir);
            move_object(state, player, new_position)
        }
        None => state.clone()
    }
}

fn move_object(state: &State, to_move: usize, new_position: Coord) -> State {
    State {
        position: position::State {
            objects: state.position.objects.iter().map(
                |object|
                if object.id == to_move {
                    position::Position {
                        coord: new_position.clone(),
                            ..object.clone()
                    }
                }
                else {
                    object.clone()
                }
            ).collect(),
            ..state.position.clone()
        },
        ..state.clone() 
    }
}

Better, though to be honest I'm starting to suspect that I'm using some FP idioms that don't translate well into Rust. I just checked, and Rust has no tail-call optimization or persistent data structures, which call both my recursive game loop and non-destructive update code into question.

First, let's change process. Old:

fn process(state: &State) {
    match get_input() {
        Some(ch) => {
            let state1 = update(state, ch);
            draw(&state1);
            process(&state1);
        },
        None => { return; }
    }
}

New:

fn process(state: &State) {
    loop {
        match get_input() {
            Some(ch) => {
                let state = update(state, ch);
                draw(&state);
                process(&state);
            },
            None => { break; }
        }
    }
}

Less elegant, but at least we won't crash after some number of moves now.

Secondly, the state-updating functions, update and move_object. I'm torn here because our state isn't large, copies are cheap, and it's conceptually very nice to have well-defined updater functions that take one state and output a new state.

But, with all those .clone()s required, and with its mut keyword, I think I can hear Rust telling me how it wants me to implement updates. Here we go:

fn main() {
    let mut state = mkstate();
    loop {
        match get_input() {
            Some(ch) => {
                update(&mut state, ch);
                draw(&state);
            },
            None => { break; }
        }
    }
}

fn update(state: &mut State, input: char) {
    match find_player(state) {
        Some(player) => {
            let dir = key_dir(input);
            let old_position = position::position_of(state, player).unwrap();
            let new_position = vadd(old_position, dir);
            move_object(state, player, new_position);
        }
        None => {}
    }
}

fn move_object(state: &mut State, to_move: usize, new_position: Coord) {
    for object in state.position.objects.iter_mut() {
        if object.id == to_move {
            object.coord = new_position.clone();
        }
    }
}

This compiles and works, and seems to make a fair amount of sense. It's not quite as satisfying as being able to write a State → State function, but it's pretty good: functions that need to update the game take a &mut State, and those that don't, like draw(), take a &State.

It's basically the same as how const works in C++.

We can also now remove derive(Clone) almost everywhere! We still need it for Coord. And it looks like we still need derive(Default) everywhere. Still, looking better.

I'm going to leave it here for now. It seems that the Rust documentation situation has improved greatly over the past year, and there's even an official tutorial now. I'm also starting to get a better idea of what Rust is, and what it is not.

As always, if you know about Rust and you think I'm not doing something in the best way, please let me know. See you next time!