Getting started with rust

Learning to Rust

For many, Rust can be intimidating to learn. Maybe not for some people out there who are seasoned with various other low-level languages like C, C++, C#, Java, and others. Some of the people out there either catch on so easily it seems inhuman (though I feel these unicorn developers to be a myth), but many have just been in the industry from the time you had to wait overnight to let your punch cards compile. Though I prefer to assume that like me, most people scratch their heads every time they try to grok something in a new language they have never done before. And often times, fight harder than hey really need to.

At least, thats what I’m telling the inner “impostor syndrome” inside of me. That, I’m not the only one that has issues learning sometimes.

Assumptions

The only thing I assume is that you have learned at least one other language. And you understand basically what Rust is (compiled language target to replace C), and you also know what loops are, that we use letters for numbers (apparently my high school teacher was right), and what comments look like.

Setup

The best way to get rust installed, is to use rustup. In python, this is similar to say pyenv, or virtualenv. Or n/nvm in node. Please refer to their installation options if you have any issues.

Once installed, you can simply run this to get started:

$ rustup update
$ rustup install stable

For now, this is all we need. There are some more advanced things we can do but we need not worry about such things at this point.

For now, cd into the directory you want your project to be located and create your project. There are two types of projects. library and binary (the default). To start, there won’t be many differences. But for reference:

  • A binary is something to be run directly from your command line, such as the ls command. By default, this creates src/main.rs
  • A library is code that other projects (‘crates’) will make use of. today to start, we will create a binary and later change it to a library when we start writing some tests. By default this created src/lib.rs

The other big difference is that binary projects do not support integration tests. Which are those found inside a tests/ directory in your project root. But for now, that is not an issue. At this point, you should have this file structure:

$ cd $HOME/code
$ cargo new rustcode
$ tree        
.
└── rustcode
    ├── Cargo.toml
    └── src
        └── main.rs

2 directories, 2 files

$ cd rustcode

See Rust Run

You can run your newly created project.

$ cargo run      
   Compiling rustcode v0.1.0 (file:///home/shawn/code/autoferrit/rustcode)        
    Finished dev [unoptimized + debuginfo] target(s) in 0.32s
     Running `target/debug/rustcode`
Hello, world!

HUZZAH!! You have just written your first… OK, no we haven’t written anything yet. So, let’s get started. At this point, we can start by writing some actual code.

Numbers

Rust has many ways to get started. But the easiest will be simply to do some math. Let’s square some things. Not shapes, thats later. We are doing numbers remember? Right. In main.rs lets add a function:

fn squared(n: i32) -> i32 {
    let squared = n * n;
    return squared;
}

Here we specify that we will get the input of a number, n, of type i32. We will also return an i32 value. When we assign squared, Rust can infer the type based on the assignment so we need not add the type there. Now update the main function to call our code.

fn main() {
    let n = squared(5);
    println!("String: {}", n);
}

And running the program:

$ cargo run
   Compiling writing_rust v0.1.0 (file:///home/shawn/code/autoferrit/rustcode)                 
    Finished dev [unoptimized + debuginfo] target(s) in 0.23s
     Running `target/debug/writing_rust`
String: 25

Great! We have working code. But, it is actually a little verbose. in the squared function, we can actually simplify this with

fn squared(n: i32) -> i32 {
    n * n
}

This is because we could simply return the multiplication statement anyways. But in Rust, if you want to return something with the last expression of the function, you can simply write a statement without the semicolon. hence only one line with n * n. Running this will still work.

The test

Now, let’s write a simple test. It won’t take much. At the bottom of main.rs add a test

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn squared_test() {
        assert_eq!(25, squared(5));
    }
}

This does a few things. First, by default, your code in main.rs is in its own root level module. So we need to create a “sub-module” to make tests. This in-line mod tests defines this module inside of a scope using curly braces{ }. Everything in these braces is in the tests module.

With #[cfg(test)] we ensure this is only ever run in the test environment. It is ignored when running the commands like cargo run or cargo build.

In order to use anything from the super (or parent) module (everything outside of the mod tests; scope) where add_one is located, we need to import that code with a use statement. To make things easier, we use a glob import and pull in everything from the parent module. This is similar to from os import * in python. In Rust, you can almost pretend the tests module is in a different file. Which, is something we will get to later as well.

The line #[test] defines a test function to be run when running cargo test. the method names can be anything and they don’t need to have _test in their name. But naming them after the method they are testing against is good practice. Especially when we have more tests.

Now run cargo test to see your work:

$ cargo test
   Compiling writing_rust v0.1.0 (file:///home/shawn/code/autoferrit/rustcode)                 
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running target/debug/deps/writing_rust-3292fe252064be8c

running 1 test
test tests::squared_test ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

Everything works!

Code organization

Frankly, for me having all this code in the same file is not very useful in most cases. So let’s create a test file. Create a new file at src/squares.rs and move over the squares functions and the test. The file should look like this:

fn squared(n: i32) -> i32 {
    let squared = n * n;
    return squared;
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn squared_test() {
        assert_eq!(25, squared(5));
    }
}

If you run this now, you will find it breaks because now main.rs doesn’t know where the squared function is. So we need to tell Rust to include this new module, and then what to use in it. Add this to the top of main.rs

mod squared;
use squared::squared;

Running this now you will get an error

function `squared` is private

This is because Rust has good encapsulation. It only exposes module functions you have specifically exposed to be public. Right now our module is not public. So to do so, update the squared method in sqared.rs as so:

pub fn squared(n: i32) -> i32 {

By adding the pub keyword. running both cargo run and cargo test should now work as expected.

Module Directories

You can also use folders to specify modules. Create src/squared and rename src/squared.rs to src/squared/mod.rs. mod.rs is a special file telling Rust this is sort of like the main of this module. Simply creating this directory, and moving/renaming the file should still have a working run/test command.

Theres one more way we can organize these tests. Often times as your code grows, things can get messy. And when your tests are in the same file as code, messy is probably the nice way to say it. Start by creating a new file at src/squared/tests.rs and move the tests over, but removing the line with mod tests. The file should look like:

#[cfg(test)]
use super::*;

#[test]
fn squared_test() {
    assert_eq!(25, squared(5));
}

And like in main we need to tell the module where this test resides. Simply add mod tests; at the top of src/squared/mod.rs. And cargo run and cargo test will both run.

Conclusion

At this point, we are set with a very basic project. At least at a point where we could play around and add a few tests without being too lost.

I have put up the code on gitlab.com/autoferrit/writing_rust on the branch labeled 001_getting_started

Additionally, if you find issues with this article you are welcome to suggest fixes over on GitLab as well.

What other simple issues if you had getting started with rust?