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.
Part 1
Part 2
Part 3
Part 4
Part 5
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 HashMap
s. 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 int
s 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.
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 “&-pointer
s” 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 Option
s. 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.
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 Coord
s:
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?
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 int
s to isize
and our uint
s 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.
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 char
s [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!
There's an RSS feed for this series.
If you're interested, my main project these days is the video game Blackshift.
The code for this series, as of the latest update, is available here.
If you don't follow me on Twitter, I will kill myself.