Control flow
Control flow constructs are used to direct the execution path of a program based on a decision.
When expressions
The when
expression is for making decisions based on conditions that
evaluate to Bool
.
import std/io
let x = 10
when {
x > 0 => io::println("positive")
}
A catch-all can be added using the _
condition.
when {
x > 0 => io::println("positive")
_ => io::println("zero or negative")
}
It might be tempting to use True
as the catch-all condition, and while this
would work, it is idiomatic to use _
. All control flow structures (and
patterns) use _
as the catch-all, thus, _
consistently has
"always" semantics in every context it is used.
If there are multiple arms in a when
, only the first one whose condition is
satisfied is evaluated. When expressions do not have to be exhaustive (i.e.,
guarantee that at least one condition will be satisfied) when used as a
statement.
// This is valid and does not print anything
// since there is no condition that covers 0.
let x = 0
when {
x > 0 => io::println("positive")
x < 0 => io::println("negative")
}
A when
block used in expression position returns its last evaluated
expression, and thus, must be exhaustive by specifying an _
arm.
let x = 10
io::println(when {
x > 0 => "positive"
_ => "zero or negative"
})
Multiple statements can be used in a when
arm body by wrapping with {...}
.
when {
x > 0 => io::println("positive")
x < 0 => {
io::println("only works for positive numbers")
io::println("please try again")
}
}
Match expressions
A match expression is used to match against one or more patterns.
let x = Some(123)
// Get the value or a default.
let value = match x {
Some(value) => value
None => 0
}
assert_eq(value, 123)
A match
case may have a guard using when
to add a condition.
match "hello" {
message when length(message) > 10 => "long message"
_ => "short message"
}
A default _
pattern must be added. The compiler cannot check for
exhaustion when guards are involved. To the programmer, it is obvious that
either the first or second arm matches, but this is difficult for the compiler
to determine.
Like when
expressions, multiple statements can appear in arm bodies by
enclosing the body with {...}
.
Select expressions
Concurrency provides context for the select
expression.
The select
expression is a concurrency primitive used to read from one
channel that has data ready. Concurrency is covered later,
however, the example below is annotated to explain the role of channels in
concurrent processes.
import std/io
import std/task
// These are communication channels. Values sent over
// the sender side appears on the receiver side.
let (sender1, receiver1) = channel()
let (sender2, receiver2) = channel()
// Concurrent tasks can be spawned within an async block.
async() {
// These tasks simply send a string over the communication channels.
task::start() { send(sender1, "Hello") }
task::start() { send(sender2, "Greetings") }
// The select expression picks on the receivers and evaluates its body.
select {
message <- receiver1 => io::println("Receiver1 got a message: " + message)
message <- receiver2 => io::println("Receiver2 got a message: " + message)
}
}
The above will print either Receiver1 got a message: Hello
or
Receiver2 got a message: Greetings
. It will print one but not both, and the
one that is printed is non-deterministic. The one that is selected depends on
two things:
- whether the tasks that produce values have been evaluated sufficiently far to send the values over the channel, and
- the random state that the
select
expression starts with.
A channel will only be selected if it has a value ready. The select
statement
does extra work to ensure that the selected channel is random. The usefulness
of this can be illustrated by counter-example. Suppose that there is no
randomness in the selection process, and the select
expression looks at the
channels from top to bottom. It would choose the first channel that is ready,
and repeat from the top if none were found on that pass. For example, if
receiver2
has values waiting to be read, but values are constantly being sent
from sender1
to receiver1
, then it is very likely that the second arm of
the above select
could be starved, as receiver1
is always tested first and
is likely to have at least one value waiting. For this reason, select
tests
the channels in a random order that is different every time to ensure fairness.
The select
expression has the same features as other control structures.
- A default arm can be specified with
_
. - Arms can have multiple expressions by wrapping the body with
{...}
. - It returns the value of the evaluated match arm when used in expression
position. In this case, the
_
arm must be given.
When no channels have data available, the select
expression blocks until at
least one has a value available to read. A select
can be made non-blocking by
providing an arm with the _
condition that is evaluated if none of the
channels are ready.
// This select is non-blocking because of the _ arm.
// If no messages are available on the receivers then
// the _ arm is immediately evaluated.
let status = select {
message <- receiver1 => {
let prefix = "Receiver1 got a message: "
prefix + message
}
message <- receiver2 => "Receiver2 got a message: " + message
_ => "No messages were ready"
}
io::println(status)