When Ferrous Metals Corrode, pt. V

Intro

This part summarizes the sixth chapter of "Programming Rust, 2nd Edition", "Expressions".

An Expression Language

All control structures are expressions – they can produce a value. For example:

let status =
  if cpu.temperature <= MAX_TEMP {
      HttpStatus::Ok
  } else {
      HttpStatus::ServerError  // server melted
  };

Blocks and Semicolons

A block can produce a value:

let a = {
    b = b + 100;
    f(b)
}

Note there's no semicolon at the end of the block – the block will return it's last expression f(b)

All arms of an if expression must produce the same type. For an if without an else this would be the unit type ()

Declarations

The most common declaration is let, can (but doesn't have to) be used with an initialization.

Note in the fragment below the let inside the for loop creates a second var of different type which shadows the first. The type of the first variable line is Result<String, io::Error>. The second line is a String.

for line in file.lines() {
    let line = line?;
    ...
}

Function declarations fn can be nested, however they don't have lexical scoping a la Python i.e. they don't see vars of the enclosing scope. Use closures for that.

The if let form looks like this:

if let Some(a) = foobar {
    println!("Got an {}", a);
}

This is shorthand for a match with just one arm which checks for destructuring a value and executes only if that succeeded.

Loops

The while let loop is analogous to if let: it evals an expression and matches against a pattern. The while body is executed only if that match succeeded.

The .. operator produces a range, a simple struct with two fields: start and end.

A break expression exits an enclosing loop. If that is the endless loop{} you can give the break an expression too. As you'd expect a continue will start a new iteration. Both break and continue can be labeled (to deal with nested loops).

Why does Rust have that infinite loop { } construct?

In other languages you'd probably do something like a while true: ... thing that exits via a break or return

Something like this in Rust:

while true {
        if process.wait() {
            return process.exit_code();
        }
}

However the compiler here is not smart enough to figure out that the while would only ever return an i32 from the exit_code – and complains about the unit type which it thinks might be the value of the while.

The loop expression better fits this case. Some expressions such as the loop are exempt from the usual type check.

Expressions that don't finish normally are assigned the special ! type

For instance:

fn exit(code: i32) -> !  

Example for an endless loop:

fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {
    socket.listen();
    loop {
        let s = socket.accept();
        handler.handle(s);
    }
}

Function and method calls

This is a static method call:

let mut numbers = Vec::new(); 

The usual generics notation doesn't work here though, would be parsed as a less-than operator:

return Vec<i32>::with_capacity(1000); // ERRORS

Remedy: use the so-called "turbofish", ::<...>

return Vec::<i32>::with_capacity(1000);

Otoh possibly the type can be infered, so often can just use Vec::with_capacity(10)

Fields and elements

The .. type operators creates ranges:

// half-open with ..
..      // RangeFull
a ..    // RangeFrom { start: a }
.. b    // RangeTo { end: b }
a .. b  // Range { start: a, end: b }


// closed with ..=
..= b    // RangeToInclusive { end: b }
a ..= b  // RangeInclusive::new(a, b)

Type Casts

Casts are done with the as keyword:

let y = 1;
let x = y as usize;

Casting numbers work largely as you'd expect. You can always cast integers; converting to a narrower type results in truncation. Float to int rounds towards zero. Overflow will result in the largest int the var can hold – casting 1e6 to u8 will evaluate to 255

Bools and chars can be cast to integers. The converse is not true though, except for a u8 which may be cast to type char

Sometimes we don't need to cast for conversions, e.g. converting a mut reference to a non-mut reference.

Some more automatic conversions:

  • &String to &str

  • Vec<i32> to &[i32]

  • Box<Chessboard> to &Chessboard

These are called deref coercions; they work for types which implement the Deref trait. User-defined types can implement this as well.

Closures

We've seen a closure already. Here's one more example:

let is_even = |x| x % 2 == 0;

// same, with types spelt out -- must use a block 
let is_even = |x: u64| -> bool { x % 2 == 0 }; 

Coda

Having control structures as expressions reminded me of Erlang, this feels nice and functional here. In Erlang, you'd usually do a lot of pattern matching; I haven't yet looked much at real-world Rust code, but my guess would be that the if let construct would be used often here (besides the match expression of course).