Expressions - Programming Rust (2016)

Programming Rust (2016)

Chapter 6. Expressions

In this chapter, we’ll cover the expressions of Rust, the building blocks that make up the body of Rust functions. A few concepts, such as closures and iterators, are deep enough that we will dedicate a whole chapter to them later on. For now, we aim to cover as much syntax as possible in a few pages.

An expression language

Rust visually resembles the C family of languages, but this is a bit of a ruse. In C, there is a sharp distinction between expressions, bits of code which look something like this:

5 * (fahr-32) / 9

and statements, which look more like this:

for (; begin != end; ++begin) {

if (*begin == target)

break;

}

Expressions have values. Statements don’t.

Rust is what is called an expression language. This means it follows an older tradition, dating back to Lisp, where expressions do all the work.

In C, if and switch are statements. They don’t produce a value, and they can’t be used in the middle of an expression. In Rust, if and match can produce values. We already saw this in Chapter 2:

pixels[r * bounds.0 + c] =

match escapes(Complex { re: point.0, im: point.1 }, 255) {

None => 0,

Some(count) => 255 - count as u8

};

An if expression can be used to initialize a variable:

let status =

if cpu.temperature <= MAX_TEMP {

HttpStatus::Ok

} else {

HttpStatus::ServerError // server melted

};

A match expression can be passed as an argument to a function or macro:

println!("Inside the vat, you see {}.",

match vat.contents {

Some(brain) => brain.desc(),

None => "nothing of interest"

});

This explains why Rust does not have C’s ternary operator (e1 ? e2 : e3). In C, it is a handy expression-level analogue to the if statement. It would be redundant in Rust: the if expression handles both cases.

Most of the control flow tools in C are statements. In Rust, they are all expressions.

Blocks and statements

Blocks, too, are expressions. A block produces a value and can be used anywhere a value is needed.

let display_name = match post.author() {

Some(author) => author.name(),

None => {

let network_info = try!(post.get_network_metadata());

let ip = network_info.client_address();

ip.to_string()

}

};

The code after Some(author) => is the simple expression author.name(). The code after None => is a block. It makes no difference to Rust. The value of the block is the value of its last expression, ip.to_string().

Rust’s rules regarding semicolons cause mild but recurring perplexity in many programmers. Sometimes a semicolon is required, sometimes it may be dropped, sometimes it must be dropped. The rules are intuitive enough that no one ever seems to struggle with them, but to set your mind at ease, we present the full rules below. The key is that Rust does have statements after all.

The syntax of a block is:

{ stmt* expr? }

That is, a block contains zero or more statements, followed by an optional final expression. This final expression, if present, gives the block its value and type. Note that there is no semicolon after this expression.

As almost everything is an expression in Rust, there are only a few kinds of statements:

§ empty statements

§ declarations

§ expression statements

An empty statement consists of a stray semicolon, all by itself. Rust follows the tradition of C in allowing this. Empty statements do nothing except convey a slight feeling of melancholy. We mention them only for completeness.

Declarations are described in a separate section below.

That leaves the expression statement, which is simply an expression followed by a semicolon. Here are three expression statements:

dandelion_control.release_all_seeds(launch_codes);

stats.launch_count += 1;

status_message =

if self.stuck_seeds.is_empty() {

"launch ok!".to_string()

} else {

format!("launch error: {} stuck seeds", self.stuck_seeds.len())

};

The semicolon that marks the end of an expression statement may be omitted if the expression ends with } and its type is (). (This rule is necessary to permit, for example, an if block followed by another statement, with no semicolon between.)

And that is all: those are Rust’s semicolon rules. The resulting language is both flexible and readable. If a block looks like C code, with semicolons in all the familiar places, then it will run just like a C block, and its value is (). To make a block produce a value, add an expression at the end without a trailing semicolon.

Declarations

A let declaration is the most common kind of declaration. We have already shown many of these. They declare local variables.

let binding: type = expr;

The type and initializer are optional. The semicolon is required.

The scope of a let variable starts immediately after the let declaration and extends to the end of the block. This matters when you have two different variables with the same name:

for line in buf_read.lines() {

let line = try!(line);

...

}

Here the loop variable, line, is an io::Result<String>. Inside the loop, try!() returns early if an IO error occurred. Otherwise, the String is stored in a new variable, also called line. The new variable shadows the old one. We could have given the two variables different names, but in this case, using line for both is fine. Taking the wrapper off of something doesn’t necessarily mean it needs a new name.

A let declaration can declare a variable without initializing it. The variable can then be initialized with a later assignment. This is occasionally useful, because sometimes a variable should be initialized from the middle of some sort of control flow construct:

let name;

if self.has_nickname() {

name = self.nickname();

} else {

name = generate_unique_name();

self.register(&name);

}

Here there are two different ways the local variable name might be initialized, but either way it will be initialized exactly once, so name does not need to be declared mut.

It’s an error to use a variable before it’s initialized. (This is closely related to the error of using a value after it’s been moved. Rust really wants you to use values only while they exist!)

A block can also contain item declarations. An item is simply any declaration that could appear globally in a program or module, such as a fn, struct, or use.

Later chapters will cover items in detail. For now, fn makes a sufficient example. Any block may contain a fn:

fn show_files() -> Result<()> {

let mut v = vec![];

...

fn cmp_by_timestamp_then_name(a: &FileInfo, b: &FileInfo) -> Ordering {

if a.mtime != b.mtime {

a.mtime.cmp(&b.mtime).reverse()

} else {

a.path.cmp(&b.path)

}

}

v.sort_by(cmp_by_timestamp_then_name);

...

}

When a fn is declared inside a block, its scope is the entire block—that is, it can be used throughout the enclosing block. But a nested fn is not a closure. It cannot access local variables or arguments that happen to be in scope.

A block can even contain a whole module. This may seem a bit much—do we really need to be able to nest every piece of the language inside every other piece?—but as we’ll see, macros have a way of finding a use for every scrap of orthogonality the language provides.

if and match

The form of an if statement is familiar:

if condition1 {

block1

}elseif condition2 {

block2

} else{

blockn

}

Each condition must be an expression of type bool; true to form, Rust does not implicitly convert numbers or pointers to boolean values.

Unlike C, parentheses are not required around conditions. In fact, rustc will emit a warning if unnecessary parentheses are present. The curly braces, however, are required.

The else if blocks, as well as the final else, are optional. An if expression with no else block behaves exactly as though it had an empty else block.

match expressions are analogous to the C switch statement, but more flexible. A simple example:

match code {

0 => println!("OK"),

1 => println!("Wires Tangled"),

2 => println!("User Asleep"),

_ => println!("Unrecognized Error {}", code)

}

This is something a switch statement could do. Exactly one of the four arms of this match expression will execute, depending on the value of code. The wildcard pattern _ matches everything, so it serves as the default: case.

For this kind of match, the compiler generally uses a jump table, just like a switch statement. A similar optimization is applied when each arm of a match produces a constant value. In that case, the compiler builds an array of those values, and the match is compiled into an array access. Apart from a bounds check, there is no branching at all in the compiled code.

The versatility of match stems from the variety of supported patterns that can be used to the left of => in each arm. Above, each pattern is simply a constant integer. We’ve also shown match expressions that distinguish the two kinds of Option value:

match params.get("name") {

Some(name) => println!("Hello, {}!", name),

None => println!("Greetings, stranger.")

}

This is only a hint of what patterns can do. A pattern can match a range of values. In can unpack tuples. It can match against individual fields of structs. It can chase references, borrow parts of a value, and more. Rust’s patterns are a mini-language of their own. We’ll dedicate several pages to them in Chapter 7.

The general form of a match expression is:

match value {

pattern => expr,

...

}

The comma after an arm may be dropped if the expr is a block.

Rust checks the given value against each pattern in turn, starting with the first. When a pattern matches, the corresponding expr is evaluated and the match expression is complete; no further patterns are checked. At least one of the patterns must match. Rust prohibits match expressions that do not cover all possible values:

let score = match card.rank {

Jack => 10,

Queen => 10,

Ace => 11

}; // error: non-exhaustive patterns

All blocks of an if expression must produce values of the same type:

let suggested_pet =

if with_wings { Pet::Buzzard } else { Pet::Hyena }; // ok

let favorite_number =

if user.is_hobbit() { "eleventy-one" } else { 9 }; // error

Similarly, all arms of a match expression must have the same type:

let suggested_pet =

match favorites.element {

Fire => Pet::RedPanda,

Air => Pet::Buffalo,

Water => Pet::Orca,

_ => None // error: incompatible types

};

There is one more if form, the if let expression:

iflet pattern = expr {

block1

}else{

block2

}

The given expr either matches the pattern, in which case block1 runs, or it doesn’t, and block2 runs. This is shorthand for a match expression with just one pattern:

match expr {

pattern =>{ block1 }

_=>{ block2 }

}

Loops

There are four looping expressions:

while condition {

block

}

whilelet pattern = expr {

block

}

loop{

block

}

for binding in collection {

block

}

Loops are expressions in Rust, but they don’t produce useful values. The value of a loop is ().

A while loop behaves exactly like the C equivalent, except that again, the condition must be of the exact type bool.

The while let loop is analogous to if let. At the beginning of each loop iteration, the result of expr either matches the given pattern, in which case the block runs, or it doesn’t, in which case the loop exits.

Use loop to write infinite loops. It executes the block repeatedly forever (or until a break or return is reached, or the thread panics).

A for loop evaluates the collection expression, then evaluates the block once for each value in the collection. Many collection types are supported. The standard C for loop:

for (int i = 0; i < 20; i++) {

printf("%d\n", i);

}

is written like this in Rust:

for i in 0..20 {

println!("{}", i);

}

As in C, the last number printed is 19.

The .. operator produces a range, a simple struct with two fields: start and end. 0..20 is the same as std::ops::Range { start: 0, end: 20 }. Ranges can be used with for loops because Range is an iterable type: it implements the std::iter::IntoIterator trait, which we’ll discuss in [Link to Come]. The standard collections are all iterable, as are arrays and slices.

In keeping with Rust’s move semantics, a for loop over a value consumes the value:

let strings: Vec<String> = error_messages();

for s in strings { // each String is moved into s here...

println!("{}", s);

} // ...and dropped here

println!("{} error(s)", strings.len()); // error: use of moved value

This can be inconvenient. The easy remedy is to loop over a reference to the collection instead. The loop variable, then, will be a reference to each item in the collection:

for ps in &strings {

println!("String {:?} is at address {:p}.", *ps, ps);

}

Here the type of &strings is &Vec<String> and the type of ps is &String.

Iterating over a mut reference provides a mut reference to each element:

for p in &mut strings {

p.push('\n'); // add a newline to each string

}

Chapter iterators covers for loops in greater detail and shows many other ways to use iterators.

A break expression exits an enclosing loop. (In Rust, break works only in loops. It is not necessary in match expressions, which are unlike switch statements in this regard.)

A continue expression jumps to the next loop iteration.

A loop can be labeled with a lifetime. In the example below, 'search: is a label for the outer for loop. Thus break 'search exits that loop, not the inner loop.

'search:

for room in apartment {

for spot in room.hiding_spots() {

if spot.contains(keys) {

println!("Your keys are {} in the {}.", spot, room);

break 'search;

}

}

}

Labels can also be used with continue.

return expressions

A return expression exits the current function, returning a value to the caller.

return without a value is shorthand for return ():

fn f() { // return type omitted: defaults to ()

return; // return value omitted: defaults to ()

}

return is familiar enough in statement-oriented code. In expression-oriented code it can be puzzling at first, so it’s worth spending a moment on an example. Back in Chapter 2, we used the try!() macro to check for errors after calling a function that can fail:

let output = try!(File::create(filename));

This is shorthand for the following match expression:

let output = match File::create(filename) {

Ok(val) => val,

Err(err) => return Err(err)

};

The match expression first calls File::create(filename). If that returns Ok(val), then the whole match expression evaluates to val, so val is stored in output and we continue with the next line of code.

Otherwise, we hit the return expression, and it doesn’t matter that we’re in the middle of evaluating a match expression in order to determine the value of the variable output. We abandon all of that and exit the enclosing function, returning whatever error we got from File::create(). This is how try!() magically propagates errors. There is nothing magic about the control flow; it’s just using standard Rust parts.

Chapter macros explains in detail how try!(File::create(filename)) is expanded into a match expression.

Why Rust has loop

Several pieces of the Rust compiler analyze the flow of control through your program.

§ Rust checks that every path through a function returns a value of the expected return type. To do this correctly, it needs to know whether or not it’s possible to reach the end of the function.

§ Rust checks that local variables are never used uninitialized. This entails checking every path through a function to make sure there’s no way to reach a place where a variable is used without having already passed through code that initializes it.

§ Rust warns about unreachable code. Code is unreachable if no path through the function reaches it.

These are called flow-sensitive analyses. They are nothing new; Java has had a “definite assignment” analysis, similar to Rust’s, for years.

When enforcing this sort of rule, a language must strike a balance between simplicity, which makes it easier for programmers to figure out what the compiler is talking about sometimes—and cleverness, which can help eliminate false warnings and cases where the compiler rejects a perfectly safe program. Rust went for simplicity. Its flow-sensitive analyses do not examine loop conditions at all, instead simply assuming that any condition in a program can be either true or false.

This causes Rust to reject some safe programs:

fn wait_for_process(process: &mut Process) -> i32 {

while true {

if process.wait() {

return process.exit_code();

}

}

} // error: not all control paths return a value

The error here is bogus. It is not actually possible to reach the end of the function without returning a value.

The loop expression is offered as a “say-what-you-mean” solution to this problem.

Rust’s type system is affected by control flow, too. Earlier we said that all branches of an if expression must have the same type. But it would be silly to enforce this rule on blocks that end with a break or return expression, an infinite loop, or a call to panic!() orstd::process:exit(). What all those expressions have in common is that they never finish in the usual way, producing a value. A break or return exits the current block abruptly; an infinite loop never finishes at all; and so on.

So in Rust, these expressions don’t have a normal type. Expressions that don’t finish normally are assigned the special type !, and they’re exempt from the rules about types having to match. You can see ! in the function signature of std::process::exit():

pub fn exit(code: i32) -> !

The ! means that exit() never returns. It’s a divergent function.

You can write divergent functions of your own using the same syntax, and this is perfectly natural in some cases:

fn serve_forever(socket: ServerSocket, handler: ServerHandler) -> ! {

socket.listen();

loop {

let s = socket.accept();

handler.handle(s);

}

}

Of course, Rust then considers it an error if the function can return normally.

Names, paths, and use

We turn now from control flow to the other building blocks of Rust expressions: names, operators, function calls, and so on.

Since the standard library crate, std, is part of every Rust crate by default, you can refer to any standard library feature by writing out its full path:

if s1 > s2 {

::std::mem::swap(&mut s1, &mut s2);

}

This function name, ::std::mem::swap, is an absolute path, because it starts with ::. ::std refers to the top-level module of the standard library (regardless of anything else you might have declared locally with the name std). ::std::mem is a submodule within the standard library, and::std::mem::swap is a public function in that module.

You could write all your code this way, spelling out ::std::f64::consts::PI and ::std::collections::HashMap::new every time you want a circle or a dictionary. The alternative is to import features into the modules where they are used.

use std::mem::swap;

if s1 > s2 {

swap(&mut s1, &mut s2);

}

The use declaration causes the name swap to be an alias for ::std::mem::swap throughout the enclosing block or module. Paths in use declarations are automatically absolute paths, so there is no need for a leading ::.

Several names can be imported at once:

use std::collections::{HashMap, HashSet};

use std::io::prelude::*; // import all of this module's `pub` items

A few particularly handy names, like Vec and Result, are automatically imported for you: the standard prelude.

In [Link to Come], we will cover crates, modules, and use in more detail.

Closures

Rust has closures, lightweight function-like values. A closure usually consists of an argument list, given between vertical bars, followed by an expression:

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

Rust infers the argument types and return type. Alternatively, they can be specified explicitly, but in that case, the body of the closure must be a block:

let is_even = |x: u64| -> bool { x % 2 == 0 };

Closures can be called using ordinary function-call syntax:

assert_eq!(is_even(14), true);

Closures are one of Rust’s most delightful features, and there is a great deal more to be said about them. We shall say it in [Link to Come].

Function and method calls

Function calls and method calls are much like those in other languages:

let room = player.location();

Rust typically distinguishes between references and the values they refer to, but in this case, as a convenience, Rust allows either a value or a reference to the left of the dot.

So, for example, if the type of player is &Creature, then it inherits the methods of the type Creature. You don’t have to write (*player).location(). (Or player->location(), as you would in C++; there is no such syntax in Rust.) The same is true for smart pointer types, likeBox. If player is a Box<Creature>, you may call Creature methods on it directly.

When calling a method that takes its self parameter by reference, Rust implicitly borrows the appropriate kind of reference, so you don’t have to write (&player).location() either.

These conveniences apply only to the self argument. Static method calls have no self argument, so they do not automatically dereference or borrow their arguments. They are really just plain function calls:

String::from_utf8(bytes) // `bytes` must be a Vec<u8>

Occasionally a program needs to refer to a generic method or type in an expression. The usual syntax for generic types, Vec<T>, doesn’t work there:

return Vec<i32>::with_capacity(1000); // error: something about chained comparisons

let ramp = (0 .. n).collect<Vec<i32>>(); // same error

The problem is that in expressions, < is the less-than operator. The Rust compiler helpfully advises writing ::<T> instead of <T> in this case, and that solves the problem:

return Vec::<i32>::with_capacity(1000); // ok, using ::<

let ramp = (0 .. n).collect::<Vec<i32>>(); // ok, using ::<

The symbol ::< is affectionately known in the Rust community as the “Space Invader smiley”.

Alternatively, it is often possible to drop the type parameters and let Rust infer them.

return Vec::with_capacity(10); // ok, if the fn return type is Vec<i32>

let ramp: Vec<i32> = (0 .. n).collect(); // ok, variable's type is given

Fields and elements

The fields of a struct are accessed using familiar syntax. Tuples are the same except that their fields have numbers rather than names.

game.black_pawns // struct field

coords.1 // tuple field

If the value to the left of the dot is a reference or smart pointer type, it is automatically dereferenced, just as for method calls.

Square brackets access the elements of an array, a slice, or a vector:

pieces[i] // array element

The value to the left of the brackets is automatically dereferenced.

Expressions like the three shown above are called lvalues, because they can appear on the left side of an assignment:

game.black_pawns = 0x00ff0000_00000000_u64;

coords.1 = 0;

pieces[2] = Some(Piece::new(Black, Knight, coords));

Of course, this is permitted only if game, coords, and pieces are declared as mut bindings.

Extracting a slice from an array or vector is straightforward:

let second_half = &game_moves[midpoint .. end];

Here game_moves may be either an array, a slice, or a vector; the result, regardless, is a borrowed slice of length end - midpoint. game_moves is considered borrowed for the lifetime of second_half.

The .. operator allows either operand to be omitted; it produces up to four different types of object depending on which operands are present:

.. // RangeFull

a .. // RangeFrom { start: a }

.. b // RangeTo { end: b }

a .. b // Range { start: a, end: b }

Only the last form is useful as an iterator in a for loop, since a loop must have somewhere to start and we typically also want it, eventually, to end. But in array slicing, the other forms are useful too. If the start or end of the range is omitted, it defaults to the start or end of the data being sliced.

So an implementation of quicksort, the classic divide-and-conquer sorting algorithm, might look, in part, like this:

fn quicksort<T: Ord>(slice: &mut [T]) {

...

// Recursively sort the front half of `slice`.

quicksort(&mut slice[.. pivot_index]);

// And the back half.

quicksort(&mut slice[pivot_index + 1 ..]);

}

Reference operators

The address-of operators, & and &mut, are covered in Chapter 5.

The unary * operator is used to access the value pointed to by a reference. As we’ve already pointed out, in many places, Rust automatically follows references, so the * operator is necessary only when we want to read or write the entire value that the reference points to.

For example, sometimes an iterator produces references, but the program needs the underlying values.

let padovan: Vec<u64> = compute_padovan_sequence(n);

for elem in &padovan {

draw_triangle(turtle, *elem);

}

In this example, the type of elem is &u64, so *elem is a u64.

Occasionally, a method uses *self = to overwrite all of a value’s fields at once:

impl Chessboard {

fn restart_game(&mut self) {

*self = Chessboard::new();

}

}

Arithmetic, bitwise, comparison, and logical operators

Rust’s binary operators are like those in many other languages. To save time, we assume familiarity with one of those languages, and focus on the few points where Rust departs from tradition.

Rust has the usual arithmetic operators, +, -, *, /, and %. As mentioned in Chapter 3, debug builds check for integer overflow, and it causes a thread panic. The standard library provides methods like a.wrapping_add(b) for unchecked arithmetic.

Unary - negates a number. It is supported only for signed integers. There is no unary +.

println!("{}", -100); // -100

println!("{}", -100u32); // error: unary negation of unsigned integer

println!("{}", +100); // error: unexpected `+`

As in C, a % b computes the remainder, or modulus, of division. The result has the same sign as the left-hand operand. Note that % can be used on floating-point numbers as well as integers:

let x = 1234.567 % 10.0; // approximately 4.567

Rust also inherits C’s bitwise integer operators, &, |, ^, <<, and >>. However, Rust uses ! instead of ~ for bitwise NOT:

let hi: u8 = 0xe0;

let lo = !hi; // 0x1f

This means that !n can’t be used on an integer n to mean “n is zero”. For that, write n == 0.

Bit shifting is always sign-extending on signed integer types and zero-extending on unsigned integer types. Since Rust has both, it does not need the >>> operator from Java and JavaScript.

Bitwise operations have higher precedence than comparisons, unlike C, so if you write x & BIT != 0, that means (x & BIT) != 0, as you probably intended. This is much more useful than C’s interpretation, x & (BIT != 0), which tests the wrong bit!

Rust’s comparison operators are ==, !=, <, <=, >, and >=. As with all the binary operators, the two operands must have the same type.

Rust also has the two short-circuiting logical operators && and ||. Both operands must have the exact type bool.

Assignment

The = operator can be used to assign to mut variables and their fields or elements. But assignment is not as common in Rust as in other languages, since variables are immutable by default.

As described in Chapter 4, assignment moves values of non-copyable types, rather than implicitly copying them.

Compound assignment is supported:

total += item.price;

The value of any assignment is (), not the value being assigned.

Rust does not have C’s increment and decrement operators ++ and --.

Type casts

Converting a value from one type to another usually requires an explicit cast in Rust. Casts use the as keyword:

let x = 17; // x is type i32

let index = x as usize; // convert to usize

Several kinds of casts are permitted.

§ Numbers may be cast from any of the builtin numeric types to any other.

Casting an integer to another integer type is always well-defined. Converting to a narrower type results in truncation. A signed integer cast to a wider type is sign-extended; an unsigned integer is zero-extended; and so on. In short, there are no surprises.

However, as of this writing, casting a large floating-point value to an integer type that is too small to represent it can lead to undefined behavior. This can cause crashes even in safe Rust. It is a bug in the compiler.

§ Values of type bool, char, or of a C-like enum type, may be cast to any integer type.

Casting in the other direction is not allowed, as bool, char, and enum types all have restrictions on their values that would have to be enforced with run-time checks. For example, casting a u16 to type char is banned because some u16 values, like 0xd800, do not correspond to Unicode code points and therefore would not make valid char values. There is a standard method, std::char::from_u32(), which performs the runtime check and returns an Option<char>; but more to the point, the need for this kind of conversion has grown rare. We typically convert whole strings or streams at once, and algorithms on Unicode text are often nontrivial and best left to libraries.

As an exception, a u8 may be cast to type char, since all integers from 0 to 255 are valid Unicode code points.

§ Pointer casts and conversions between pointers and integers are also allowed. Such casts are of little use in safe code. See [Link to Come].

We said that a conversion usually requires a cast. A few conversions involving reference types are so straightforward that the language performs them even without a cast. One trivial example is converting a mut reference to a non-mut reference.

Several more significant automatic conversions can happen, though:

§ Values of type &String auto-convert to type &str without a cast.

§ Values of type &Vec<i32> auto-convert to &[i32].

§ Values of type &Box<Chessboard> auto-convert to &Chessboard.

These are called Deref coercions, because they apply to types that implement the Deref builtin trait. Rust performs these conversions automatically for values that otherwise wouldn’t quite be the right type for the function argument they’re being passed to, the variable they’re being assigned to, and so on. We’ll revisit the Deref trait in [Link to Come].

Precedence and associativity

Table 1 gives a summary of Rust expression syntax.

Expression type

Example

Related traits

array literal

[1, 2, 3]

repeat array literal

[0; 50]

tuple

(6, "crullers")

grouping

(2 + 2)

block

{ f(); g() }

control flow expressions

if ok { f() }

if ok { 1 } else { 0 }

if let Some(x) = f() { x } else { 0 }

match x { None => 0, _ => 1 }

for v in e { f(v); }

std::iter::IntoIterator

while ok { ok = f(); }

while let Some(x) = it.next() { f(x); }

loop { next_event(); }

break

continue

return 0

macro invocation

println!("ok")

closure

|x, y| x + y

path

std::f64::consts::PI

struct literal

Point {x: 0, y: 0}

tuple field access

pair.0

Deref, DerefMut

struct field access

point.x

Deref, DerefMut

method call

point.translate(50, 50)

Deref, DerefMut

function call

stdin()

Fn(Arg0, ...) -> T,

FnMut(Arg0, ...) -> T,

FnOnce(Arg0, ...) -> T

index

arr[0]

Index, IndexMut

Deref, DerefMut

logical/bitwise NOT

!ok

Not

negation

-num

Neg

dereference

*ptr

Deref, DerefMut

borrow

&val

type cast

x as u32

multiplication

n * 2

Mul

division

n / 2

Div

remainder (modulus)

n % 2

Rem

addition

n + 1

Add

subtraction

n - 1

Sub

left shift

n << 1

Shl

right shift

n >> 1

Shr

bitwise AND

n & 1

BitAnd

bitwise exclusive OR

n ^ 1

BitXor

bitwise OR

n | 1

BitOr

less than

n < 1

std::cmp::PartialOrd

less than or equal

n <= 1

std::cmp::PartialOrd

greater than

n > 1

std::cmp::PartialOrd

greater than or equal

n >= 1

std::cmp::PartialOrd

equal

n == 1

std::cmp::PartialEq

not equal

n != 1

std::cmp::PartialEq

logical AND

x.ok && y.ok

logical OR

x.ok || backup.ok

range

start .. stop

assignment

x = val

compound assignment

x += 1

Table 6-1. Table 1 - Expressions

All of the operators that can usefully be chained are left-associative. That is, a chain of operations like a - b - c groups like (a - b) - c, not a - (b - c). The operators that can be chained in this way are all the ones you might expect:

* / % + - << >> & ^ | && ||

Unlike C, assignment can’t be chained: a = b = 3 does not assign the value 3 to both a and b. Assignment is rare enough in Rust that you won’t miss this shorthand.

The comparison operators, as, and the range operator .. can’t be chained at all.

Onward

Expressions are what we think of as “running code”. They’re the part of a Rust program that compiles to machine instructions. Yet they are a small fraction of the whole language.

The same is true in most programming languages. The first job of a program is to run, but that’s not its only job. Programs have to communicate. They have to be testable. They have to stay organized and flexible, so that they can continue to evolve. They have to interoperate with code and services built by other teams. And even just to run, programs in a statically typed language like Rust need some more tools for organizing data than just tuples and arrays.

In the next two chapters, we present structs and enums, the user-defined types of Rust. They’re a vital tool in all of the above aspects of programming.

(A quick note for Early Access readers: As of this writing, the chapter on structs is not released yet. We’ll get it to you when it’s ready.)