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 + 100;
b
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) -> ! {
.listen();
socketloop {
let s = socket.accept();
.handle(s);
handler}
}
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
.. // RangeFrom { start: a }
a .. b // RangeTo { end: b }
.. b // Range { start: a, end: b }
a
// closed with ..=
..= b // RangeToInclusive { end: b }
..= b // RangeInclusive::new(a, b) a
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).