loading...

Deadbeef? Just say no. Let's learn to build a small Rust app to find out what words can you spell with the letters A-F

timclicks profile image Tim McNamara Updated on ・3 min read

I'm always really irritated when I see "babe" and "deadbeef" (the worst) in code examples that use hexadecimal numbers. It's actually not that funny.

You know you can do the dab in hex, right?

Doing the Dab

Photo: User:Gokudabbing / Wikipedia

What else is there? Here's a selection (the full list is at the bottom of the post).

Nouns

  • ace (perhaps that should be an adjective, it's time for a 90s linguistic revival)
  • bead
  • bee
  • cab
  • dad
  • fad

Adjectives

  • decaf
  • deaf
  • dead
  • deed
  • faded

Verbs

  • add
  • cede
  • fade
  • dab
  • feed

How do we find them?

Here's the full source code. If you're new to Rust, keep reading&mdashlI explain what's going on below.

use std::fs::File;
use std::io::{BufReader, BufRead};

fn main() -> std::io::Result<()> {
    let f = File::open("/usr/share/dict/words")?;
    let reader = BufReader::new(f);

    'lines: for line in reader.lines() {
        let word = line.unwrap();
        for byte in word.bytes() {
            match byte {
                b'A' | b'B' | b'C' | b'D' | b'E' | b'F' |
                b'a' | b'b' | b'c' | b'd' | b'e' | b'f' => continue,
                _ => continue 'lines,
            }
        }

        if word.len() > 2 {
            println!("{}", word);
        }
    };

    Ok(())
}

Let's break the example down. We start with imports.

use std::fs::File;
use std::io::{BufReader, BufRead};

We're bringing some of the standard library into local scope. That gives us the ability to open files. File can open them. A BufReader can read them efficiently. We also need to import BufReader's "traits" along with it to avoid name clashes. Traits define methods, and we can sort of pick and mix the methods that we want for any given type.

Hint: If you haven't encountered the term trait before, think of it as an Abstract Base Class or an interface. If you're new to programming and haven't heard of those two either, that's ok. It's not essential to know what a trait is for the rest of the post.

fn main() -> std::io::Result<()> {
    // ...
    Ok(())
}

The std::io::Result<()> type is a bit crazy, sorry about that. It's an indicator that something might fail under the hood—like the hard drive might break or we could trigger a permissions error—and Rust should be prepared for that.

Adding Ok(()) at the end is our main() function saying "everything went fine". We are returning Ok with a () type, which has the fun name "unit". It is a placeholder value.

let f = File::open("/usr/share/dict/words")?;

I run Ubuntu, which includes a list of valid-ish words as a plain text file. We open that up. If we do end up triggering a permissions error, or the file is deleted, the ? at the end will propagate the error and immediately exit the main() function.

let reader = BufReader::new(f);

We create a BufReader that knows how to read from File objects efficiently. It's called BufReader because it has contains an internal in-memory buffer that can and then ask it to inject anything it reads into the string we create next.

Now comes the best bits, or why I love Rust:

    'lines: for line in reader.lines() {
        let word = line.unwrap();
        for byte in word.bytes() {
            match byte {
                b'A' | b'B' | b'C' | b'D' | b'E' | b'F' |
                b'a' | b'b' | b'c' | b'd' | b'e' | b'f' => continue,
                _ => continue 'lines,
            }
        }

        if word.len() > 2 {
            println!("{}", word);
        }
    };

This chunk of code is a bit of a showoff. We define a named loop, 'lines that can be used later to immediately abort from a word that contains a character outside of our limited alphabet. And then it uses the match syntax to elegantly match bytes that we care about. (The b prefix on all of those literals indicates to Rust that they should be treated as 8-bit integers, not as characters or strings - for the details check a Rust book)

Compiling the code

Let's see the result!

First let's build a new project:

$ cargo new hexwords
     Created binary (application) `hexwords` package
$ tree hexwords
hexwords
├── Cargo.toml
└── src
    └── main.rs

Now copy the source code into the main.rs file. Once that's done, we can run it.

$ cd hexwords

$ cargo run -q
Abe
Ada
BBB
Bede
Beebe
Dacca
Dada
Dec
Decca
Dee
Edda
Feb
abed
accede
acceded
ace
aced
add
added
baa
baaed
babe
bad
bade
bead
beaded
bed
bedded
bee
beef
beefed
cab
cabbed
cad
cede
ceded
dab
dabbed
dad
dead
deaf
deb
decade
decaf
deed
deeded
deface
defaced
ebb
ebbed
efface
effaced
facade
face
faced
fad
fade
faded
fed
fee
feed

Voilà!

Full source code here:

https://github.com/timClicks/hexwords

Posted on by:

Discussion

markdown guide