Your Keep in Rust

The program you already wrote, in the language that asks the most and gives the most back.

This is the longest of the three guides, and the one worth the most. Rust is where you find out what the machine is actually doing, and it is the language most likely to change how you think about programs.

It is also the one people warn you about. That warning is half right. There is exactly one hard new idea, it is genuinely hard, and the first month is frustrating in a way that Swift and Go are not. Everything else you already know — more of it than you would guess, and more than either of the other two kept.

Get it in front of you

You have a shortcut here that the other two languages do not give you. lux build has been compiling your keep through Rust the whole time:

$ lux build world.lux
built ./world
$ ./world

That binary is your keep, compiled from Rust, and it needs no lux on the machine to run. If you have ever used lux build, you have already shipped a Rust program.

To read the Rust rather than just run it:

$ lux convert rust world.lux > main.rs
$ rustc main.rs && ./main

As with the other two, the top of the file is a prelude lux wrote so that printing and file handling behave the way they do under lux run. Skip it. Your program starts at the struct, and runs to the end of the file in the order you wrote it.

#[derive(Debug, Clone, PartialEq)]
struct World {
    room: Room,
    items: Vec<String>,
    door_open: bool,
    playing: bool,
}

Two small things before the big one. Your doorOpen became door_open, because Rust names things with underscores rather than humps and lux follows the local custom. And that line above the struct is Rust asking the compiler to write some code for you — Clone means "this can be copied on request", PartialEq means "this can be compared with ==", Debug means "this can be printed for a programmer to look at". In lux all three were simply true of everything. In Rust they are capabilities you ask for by name.

Everything lux taught you, kept

If you have read the Go guide, you know that Go hands two of lux's guarantees back to you — it has no enums of the kind you know, and Option has nowhere to live, so you rebuild both by hand. Rust goes the other way. Every one of them survives, under its own name.

Your enum is an enum:

enum Room {
    Entrance,
    Hall,
    Cellar,
    Vault,
    Chamber,
}

Your match is a match:

fn describe(room: Room) -> String {
    return match room {
        Room::Entrance => "You stand at the mouth of an old stone keep…".to_string(),
        Room::Hall => "A wide hall, its banners long rotted…".to_string(),
    };
}

And Option is Option, with some and none capitalised into Some and None:

fn exit(room: Room, dir: String) -> Option<Room> {
    return match room {
        Room::Entrance => match dir.as_str() {
            "north" => Some(Room::Hall.clone()),
            _ => None,
        },
        …
    };
}

Which means the discipline you built in lux — a closed set of places, an arm for every one, and a missing value you are made to open — is not a lux habit you are leaving behind. It is a Rust habit you already have.

And one of them Rust does better

There is a real upgrade here, and it fixes the weakest thing about lux.

In lux, forgetting a case in a match is caught when that match runs. Your keep starts up fine and stops the moment something reaches the gap. Rust catches it before your program runs at all:

error[E0004]: non-exhaustive patterns: `Room::Tower` not covered
 --> keep.rs:4:11
  |
4 |     match r {
  |           ^ pattern `Room::Tower` not covered
  |
note: `Room` defined here

Add a room and the compiler hands you the complete list of everywhere you have not finished, before you play a single turn. That is the to-do list lux gave you, delivered earlier and all at once.

Who owns this value

The one hard new idea

Here is the whole of it, and it is one sentence: in Rust, every value has exactly one owner, and handing it to somebody else gives it away.

That is not how lux worked. In lux, handing a value to a function gave that function a copy, always. The original stayed yours, untouched, and two names could never mean the same thing. Rust does not copy — it moves. After you hand something over, you no longer have it.

You will meet this within your first hour, and it will be in a function you recognise. Here is takeThing — pick something up, then say what you picked up — written the natural way:

fn take(pack: Vec<String>, thing: String) -> Vec<String> {
    let mut p = pack;
    p.push(thing);
    println!("You take the {}.", thing);
    return p;
}

That does not compile. And read what Rust says about it, because it is one of the best error messages in any language:

error[E0382]: borrow of moved value: `thing`
  |
1 | fn take(pack: Vec<String>, thing: String) -> Vec<String> {
  |                            ----- move occurs because `thing` has
  |                                  type `String`, which does not
  |                                  implement the `Copy` trait
3 |     p.push(thing);
  |            ----- value moved here
4 |     println!("You take the {}.", thing);
  |                                  ^^^^^ value borrowed here after move
  |
help: consider cloning the value if the performance cost is acceptable

Read it slowly. It tells you what happened (the value moved), where it happened (line 3, pushing it into the pack), where you tried to use it afterwards (line 4), why it applies to this type and not to an int, and what to do about it. Rust's errors are like this most of the time, and they are the reason the language is learnable at all.

What went wrong: pushing thing into the pack gave the pack the string. The pack owns it now. Line 4 asks for something you handed away one line earlier.

There are two ways out and you will use both.

Give away a copy instead. That is what .clone() does, and it is what the error suggests:

p.push(thing.clone());
println!("You take the {}.", thing);   // still yours

Or lend it instead of giving it. An & means "let them look at it, do not hand it over" — Rust calls this borrowing, and it is the more Rust-like answer:

fn take(mut pack: Vec<String>, thing: &str) -> Vec<String> {
    println!("You take the {}.", thing);
    pack.push(thing.to_string());
    pack
}

Why it is full of clone

Now open your translated keep again and look at take_thing. It is dense with .clone():

let mut pack = w.items.clone();
pack.push(thing.clone());
return World { room: w.room.clone(), items: pack.clone(), … };

And walk, four lines of your lux, comes out like this:

fn walk(w: World, dir: String) -> World {
    return match exit(w.room.clone(), dir.clone()) {
        Some(r) => try_enter(w.clone(), r.clone()),
        None => cant_go(w.clone()),
    };
}

That is the translator being careful, not a style to copy. Your lux was written against a promise — every value handed over is a copy, and nothing you do inside a function can reach back out. The only way to keep that promise in a language that moves by default is to clone at every point lux would have copied.

A Rust programmer writing this from scratch would clone almost nowhere, and would pass &World and &str around instead. You can see the translator itself doing that where it safely can — has takes a borrow already:

fn has(items: &Vec<String>, thing: String) -> bool {
    for it in (*items).clone() {
        if it == thing {
            return true;
        }
    }
    return false;
}

Written by hand, that whole function is one line, and it borrows everything:

fn has(items: &[String], thing: &str) -> bool {
    items.iter().any(|it| it == thing)
}

So read the clones as a translation artefact and a useful map: every .clone() in that file marks a spot where lux gave you a copy for free and Rust is going to make you decide.

A type that contains itself

One more thing you will meet, and unlike the rest of this page it is not in your keep — it turns up the day you build something tree-shaped, which for most people is soon.

A list is either nothing, or a value followed by another list. In lux that sentence is the enum, exactly as you would say it out loud:

enum List {
    nil
    cons(head: int, tail: List)
}

In Rust that will not compile as written, and the reason is worth understanding rather than memorising. Rust wants to know how big a List is, and this one has no answer: a List contains a List, which contains a List. The fix is to say "not the thing itself, a pointer to it," and Rust spells that Box:

enum List {
    Nil,
    Cons(i64, Box<List>),
}

A Box is one value living somewhere else, with a known-size handle to it here. Building one is Box::new(…). That is the idea, and it is the first of several places where Rust makes you say something about memory that lux and Swift and Go all decided for you.

Worth knowing: all three targets solve this, each in its own way — a Box in Rust, an indirect enum in Swift, an interface in Go — and none of it shows up in the lux. That is the clearest small example of what a stepping stone is for.

The walls, gone

The things that told you lux had run out are all ordinary here.

Text that behaves, the reason typing paris got you marked wrong. lux can split and search a string and that is where it stops; Rust hands you the rest of the job, and a hundred more like it:

answer == "Paris".to_lowercase()

Looking a room up instead of matching down a list — Rust calls it a HashMap, and asking for a key that is not there hands you back an Option, so the lesson lux taught you still holds.

More than one file, and more than that: cargo, Rust's project tool, gives you other people's code with one line in a file. That is the thing that will make you fastest.

And functions as values. That |it| it == thing in the one-line has is a function written inline and handed to another function — the thing lux has no version of at all. It is what makes iter().any(), map, and filter possible, and it will change how your code looks more than any other single feature.

The first month

Everything above is true and so is this: Rust will be harder than lux for a while, in a specific and survivable way.

You will fight the compiler. Not occasionally — daily, at first, over things that felt obvious. This is not you being bad at it. Every Rust programmer went through the same weeks, and there is a widely used phrase for it, which tells you how normal it is.

Three things make it shorter.

Read the whole error. Rust's messages carry the cause, the line, the reason and usually a fix. They are long because they are complete, and skipping to the last line is the single most common way to stay stuck.

Clone your way out when you are stuck. Adding .clone() to make something compile is not cheating and not a permanent decision. It costs a little speed you cannot yet measure, and it lets you keep building instead of stopping to win an argument. Take them out later when you understand why they were there. Your translated keep is doing exactly this.

Expect the payoff to be delayed. The reward for ownership is not on the day you learn it. It is later: a program that runs very fast, that does not crash in the ways other programs crash, and that you can change without wondering what else you broke. People who stay with Rust generally say the same thing — the first month is the worst of it, and then it stops being an argument and starts being a tool.

Where to go from here

Build the thing you were blocked on, in Rust, starting from the keep you already have. You will look things up as you need them and remember them, because you wanted them first.

Three things to reach for early. cargo, on day one — start a project with it rather than driving rustc by hand, because everything in Rust assumes you are using it. Borrowing, properly and on purpose, once your first ownership errors have stopped being a surprise; that is where the language starts paying you back. And Result with the ? operator, which is the same failure-is-a-value idea lux gave you, with a single character that hands the failure upward instead of nesting your matches.

What you will not have to relearn: types, functions as boundaries, enums with a closed set of cases, exhaustive matching, missing as a shape rather than a hole, and building a new value instead of poking an old one. Rust keeps all of it. Of the three languages lux translates into, this is the one that agrees with lux most about what a program should be — it simply asks one more question, and that question is the reason to come.

In your terminal: lux build for a native binary through Rust · lux convert rust on anything you want to see translated. On the web: When lux Feels Small is the guide to the move itself, Swift and Go are the same trip into the other two, and What lux Leaves Out explains why lux was shaped this way in the first place.