Blocks and expressions

Despite being a mix of statements and expressions, Rust is primarily an expression-oriented language. This means that most constructs are expressions that return a value. It's also a language that uses C-like braces {}, to introduce new scope for variables in a program. Let's get these concepts straight before we talk more about them later in this chapter.

A block expression (hereby referred as blocks) is any item that starts with { and ends with }. In Rust, they include if else expressions, match expressions, while loops, loops, bare {} blocks, functions, methods, and closures, and all of them return a value which is the last line of the expression. If you put a semicolon in the last expression, the block expressions default to a return value of the unit () type.

A related concept to blocks is the scope. A scope is introduced whenever a new block is created. When we a new block and create any variable bindings within it, the bindings are confined to that scope and any reference to them is valid only within the scope bounds. It's like a new environment for variables to live in, isolated from the others. Items such as functions, impl blocks, bare blocks, if else expressions, match expressions, functions, and closures introduce new scope in Rust. Within a block/scope, we can declare structs, enums, modules, traits and their implementations, and even blocks. Every Rust program starts with one root scope, which is the scope introduced by the main function. Within that, many nested scopes can be created. The main scope becomes the parent scope for all inner scopes declared. Consider the following snippet:

// scopes.rs

fn main() {
let mut b = 4;
{
let mut a = 34 + b;
a += 1;
}

b = a;
}

We used a bare block {}, to introduce a new inner scope and created a variable a. Following the end of the scope, we are trying to assign b to the value of a, which comes from the inner scope. Rust throws a compile time error saying cannot find value `a` in this scope . The parent scope from main does not know anything about a as it comes from the inner scope. This property of scopes is also used sometimes to control how long we want a reference to be valid, as we saw in Chapter 5, Memory Management and Safety.

But the inner scope can access values from their parent scope. Because of that, it is possible to write 34 + b within our inner scope.

Now we come to expressions. We can benefit from their property of returning a value and that they must be of the same type in all branches. This results in very concise code. For example, consider this snippet:

// block_expr.rs

fn main() {
// using bare blocks to do multiple things at once
let precompute = {
let a = (-34i64).abs();
let b = 345i64.pow(3);
let c = 3;
a + b + c
};

// match expressions
let result_msg = match precompute {
42 => "done",
a if a % 2 == 0 => "continue",
_ => panic!("Oh no !")
};

println!("{}", result_msg);
}

We can use bare blocks to chunk several lines of code together and assign the value at the end with an implicit return of the a + b + c expression to precompute as shown previously. Match expressions can also assign and return values from their match arms directly.

Note: Being similar to the switch statement in C, match arms in Rust do not suffer from the case fall through side effect that results in lots of bugs in C code.

The C switch case requires every case statement within the switch block to have a break if we want to bail out after running the code in that case. If the break is not present, any case statement following that is also executed, which is called the fall-through behavior. A match expression, on the other hand, is guaranteed to evaluate only one of the match arms.

If else expressions provide the same conciseness:

// if_expr.rs

fn compute(i: i32) -> i32 {
2 * i
}

fn main() {
let result_msg = "done";

// if expression assignments
let result = if result_msg == "done" {
let some_work = compute(8);
let stuff = compute(4);
compute(2) + stuff // last expression gets assigned to result
} else {
compute(1)
};

println!("{}", result);
}

In statement-based languages such as Python, you would write something like this for the preceding snippet:

result = None
if (state == "continue"):
let stuff = work()
result = compute_next_result() + stuff
else:
result = compute_last_result()

In the Python code, we had to declare result beforehand, followed by doing separate assignments in the if else branch. Rust is more concise here, with the assignment being done as a result of the if else expression. Also, in Python, you can forget to assign a value to a variable in either of the branches and the variable may be left uninitialized. Rust will report at compile time if you return and assign something from the if block and either miss or return a different type from the else block.

As an added note, Rust also supports declaring uninitialized variables:

fn main() {
let mut a: i32;
println!("{:?}", a); // error
a = 23;
println!("{:?}", a); // fine now
}

But they need to be initialized before we use them. If an uninitialized variable is attempted to be read from later, Rust will forbid that and report at compile time that the variable must be initialized:

   Compiling playground v0.0.1 (file:///playground)
error[E0381]: use of possibly uninitialized variable: `a`
--> src/main.rs:7:22
|
7 | println!("{:?}", a);
| ^ use of possibly uninitialized `a`
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset