Discovering Rust

Posted by Nicolas Kosinski on 2018-06-10 Translations: fr

As a Java developer, I want to learn bits of Rust, so that I can deeply understand this sentence quoted from rust-lang.org*:

Rust is a systems programming language that runs blazingly fast, prevents segfaults, and guarantees thread safety

*: did you notice the "agile stories meme"? 😉

How I started learning Rust

The Rust Programming Language (2nd edition) is a great free on-line book with pragmatic examples and small projects that are progressively implemented (a CLI tool and a web server).

After reading parts of this book, I ported a personal project named hubstats from Clojure to Rust. Hubstats is a command line tool I wrote in Clojure that calls GitHub API and displays pull requests summaries in the standard output. I just converted this project in Rust: pullpito.

Porting existing code was an enjoyable way to learn Rust since I did not had to think about the "what" (display pull requests information) and the "how" (call the GitHub API): I just had to focus on Rust coding!

My first impressions

Rust code runs fast! For instance, let's compare running a few pullpito's unit tests that run in half a second:

pullpito $> time (cargo test --quiet)

running 8 tests
# snip
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out

( cargo test --quiet; )  0.43s user 0.21s system 96% cpu 0.665 total

On the other hand, hubstats' tests run in 10 seconds:

hubstats $> time (lein test)
# snip

Ran 3 tests containing 20 assertions.
0 failures, 0 errors.
( lein test; )  10.86s user 0.70s system 129% cpu 8.923 total

Note: the cargo command launches Cargo which is Rust's build tool ; the lein command launches Leiningen which is Clojure's build tool.

Now let's compare the CLI tool executions. pullpito runs in 20 milliseconds:

pullpito $> time (cargo run --quiet python/peps)
pull requests for "python/peps" ->
  opened per author:
    brainwane: 1
  commented per author:
    the-knights-who-say-ni: 1
    stevendaprano: 2
    pradyunsg: 2
    gvanrossum: 1
    6502: 1
    Rosuav: 1
    brainwane: 1
  closed per author:
    markshannon: 1

( cargo run --quiet python/peps; )  0.22s user 0.09s system 20% cpu 1.524 total

...whereas hubstats runs in 11 seconds (it should probably be optimized 😇):

hubstats $> time (lein run --organization python --repository peps)
pull requests for python/peps ->
    since 2018-05-15T05:35:57Z
        8 opened / 8 closed / 2 commented (15 comments)
        opened per author:  {encukou 2, willingc 1, jdemeyer 1, gvanrossum 1, ethanhs 1, daxm 1, brainwane 1}
        comments per author:  {tim-one 4, ethanhs 3, vlasovskikh 2, gvanrossum 2, JelleZijlstra 2, ilevkivskyi 1, Rosuav 1}
        closed per author:  {brettcannon 4, markshannon 3, encukou 1}
( lein run --organization python --repository peps; )  11.30s user 0.77s system 66% cpu 18.160 total

What about compilation? The first compilation is slow because all dependencies have to be compiled. For instance, pullpito initially compiles in 40 seconds, on my machine:

pullpito $> time (cargo clean && cargo build)
Compiling void v1.0.2
   Compiling byteorder v1.2.2
   Compiling serde v1.0.37
   Compiling scoped-tls v0.1.
# snip
    Finished dev [unoptimized + debuginfo] target(s) in 41.53 secs
( cargo clean && cargo build; )  213.99s user 16.77s system 552% cpu 41.788 total

But Rust has an incremental compiler, so subsequent compilations will be immediate if code does not change:

pullpito $> cargo build
    Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs

On the other hand, hubstats compiles in 40 seconds (dependencies are not compiled):

hubstats $> time (lein clean && lein uberjar)
# snip
( lein clean && lein uberjar; )  37.55s user 6.49s system 223% cpu 19.750 total


About Rust, the language


Ownership, OMG! 😱

Rust has a particular way to manage memory. Instead of using a garbage collector like Java or manual management like C/C++, allocated memory is automatically cleaned using the "ownership" rules:

    Each value in Rust has a variable that’s called its owner.
    There can only be one owner at a time.
    When the owner goes out of scope, the value will be dropped.

It seems easy, but in reality, all the implications are hard to understand!

Since I have not fully understood the ownership implications, I will not go any further on this topic. My current status is: fix all the compilation errors! 😇

Feel free to read more about it in the chapter "Understanding Ownership" of "The Rust Programming Language".


Immutability by default 😎

A variable is immutable, by default. It cannot be re-assigned unless explicitly declared mutable:

let name = "foo";
// name = "bar"; // Would trigger this compilation error: "error: re-assignment of immutable variable"

let mut changing_name = "bar";
changing_name = "baz";

However, immutable variables can be shadowed:

let name = "foo";
let name = "bar"; // shadowed variable


Type inference 😎

Type inference is great for conciseness:

// Type can be inferred:
let foo = "foo".to_string();
// or set explicitly:
let foo : String = "foo".to_string();


Pattern matching 😎

Rust has pattern matching... and it's cool!

let body = match body {
  Ok(body) => body,
  Err(_) => "default"
}

As far as I understand, variable borrowing makes pattern matching harder to use, as seen in the Stack Overflow. Note to self: try this out.


Tuples, enums and structures 😎

Rust has enums, tuples, and structures:

#![allow(dead_code)]

#[derive(Debug)] enum Suite {
  CLUB, DIAMOND, HEART, SPADE
}

#[derive(Debug)] enum Rank {
  Ace, King, Queen, Jack
}

#[derive(Debug)] struct Card {
  suite: Suite,
  rank: Rank,
}

Let's test it via the rusti REPL:

rusti=> Card { suite: Suite::CLUB, rank: Rank::King }
Card { suite: CLUB, rank: King }


Rust tooling


No REPL (with full Rust support) 😢

There is no official REPL (Read Eval Print Loop), and that's a pity for beginners like me!

rusti can help but does not support all recent language changes.

On-line REPLs such as repl.it can also be handy, even if limited (e.g. cannot import external dependencies aka crates).

Concise dependency descriptors (Cargo.toml) with semantic versioning 😎

Every cargo binary (aka crate):

  • has a descriptor with a semantic version. For instance, pullpito's version is 0.1.0, as declared in its Cargo.toml descriptor:
pullpito $> grep "^version =" Cargo.toml
version = "0.1.0"
  • declares the versions of its own dependencies:
pullpito $> grep -A 10 "dependencies" Cargo.toml
[dependencies]
log = "0.4"
env_logger = "0.5"
futures = "0.1"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"
chrono = { version = "0.4", features = ["serde"] }
reqwest = "0.8"

Cool and concise, isn't it?


Standard code format 😎

rustfmt can be used as a command-line tool to format code using a default style:

pullpito $> cat src/main.rs
fn main() {
  println! ("foo");
 let bar = "bar";
}

pullpito $> cargo fmt

pullpito $> cat src/main.rs
fn main() {
    println!("foo");
    let bar = "bar";
}

No more "tabs vs space vs ..." flame war! Cf. https://xkcd.com/1285/!


User-friendly compiler (mostly) 😎

The Rust compiler often makes useful suggestions in case of compilation error. Example:

#[derive(Debug)] enum Suite { CLUB }
fn main() {
    println!("{:?}", Suite.CLUB);
}

will fail compiling:

error[E0423]: expected value, found enum `Suite`
 --> src/main.rs:5:22
  |
5 |     println!("{:?}", Suite.CLUB);
  |                      ^^^^^
  |
  = note: did you mean to use one of the following variants?
          - `Suite::CLUB`


A multi-version toolchain: rustup 😎

Rust has three release channels: stable, beta, and nightly. You can natively install and use one, some or all of them. Indeed, some libraries or tools may only work on the "stable" toolchain, and others may require the "nightly" one. In that case, use the rustup command to install and use both toolchains.

For instance, I can install the nightly toolchain:

$> rustup install nightly

There are now two toolchains: stable and nightly:

$> rustup show
Default host: x86_64-apple-darwin

installed toolchains
--------------------

stable-x86_64-apple-darwin
nightly-x86_64-apple-darwin

active toolchain
----------------

stable-x86_64-apple-darwin (default)
rustc 1.25.0 (84203cac6 2018-03-25)

I can then:

  • change the default toolchain via rustup default
  • set the active toolchain via rustup set
  • use it on demand via rustup run $toolchain $cmd (example: rustup run nightly cargo build)

etc.

tags: rust