Statements are run as soon as they are encountered at runtime.
They do not evaluate to a value.
An assignment statement gives a name to a value inside the current scope.
answer = 42
The name an assignment gives must be a valid Roc lowercase name.
You can put a type annotation on the line before an assignment,
using the same name followed by : and a type:
answer : U64
answer = 42
This works the same way at the top level of a module and inside a function body. The
annotation is optional; without it, the compiler infers the type. With it, you get a
compile-time error if the value doesn't have that type, which is useful both as
documentation and for getting errors reported where you meant something to be a certain type,
rather than wherever the mismatch happens to show up later.
You can use pattern matching in assignments to do things like destructuring:
(x, y) = (1.1, 2.2)
The pattern you use here must be exhaustive. For example, the following would give an exhaustiveness error because it doesn't specify what to do if list.first() returned an Err instead of Ok:
Ok(item) = list.first()
If you can't write an exhaustive pattern-match, you can use match instead of an assignment.
Assignments inside expressions can only reference names that were assigned earlier in scope.
For example, this would be an error:
foo({
y = z + 1
z = 5
z + 1
})
However, at the top level of a module, assignments can reference each other
regardless of declaration order:
x = y + 1
y = 5
Top-level assignments can only mutually reference each other if they are all assigning to functions.
This gives an error at compile time:
x = y + 1
y = x + 1
(If it did not give an error at compile time, it would either crash or loop infinitely at runtime.)
In contrast, this gives no error because all the assignments in the cycle are assigning to functions:
x = |arg| if arg >= 1 { y(arg + 1) } else { 0 }
y = |arg| if arg <= 9 { x(arg + 1) } else { 0 }
Reassigning to an existing name is only allowed when the name was declared with
var. This is allowed:
var $foo = 0
$foo = 1
However, this gives a shadowing warning:
foo = 0
foo = 1
An expect cannot reassign a var declared outside of it. (See
control flow and variables inside expect.)
The import statement imports a type into scope from a type module.
import Color
import json.Parser
See import statements for details on how imports work.
Adding exposing and a list of names to an import brings those associated items of the
imported type into scope directly, so they can be used without the type's name in front:
import Color exposing [to_str, all]
red_str = to_str(Red) # instead of Color.to_str(Red)
The type itself is still imported as usual, so Color.to_str also continues to work.
Adding as and an uppercase name to an import makes the imported type
available under that name instead of its original name:
import json.Parser as JsonParser
parser : JsonParser
This is useful when two imported types would otherwise have the same name, or when a name is
long and used often.
An import can also bring the contents of an arbitrary file into scope, by giving the file's path
as a string, then as, then a name and a type:
import "config.json" as config_text : Str
import "logo.png" as logo_bytes : List(U8)
The path is relative to the .roc file containing the import. The file is read at compile time,
and its contents are embedded in the compiled program as a constant, so the program doesn't need
to read the file at runtime (and the file doesn't need to exist when the program runs).
The type must be either Str or List(U8):
Str gives the file's contents as a string. It's a compile-time error if the file isn't valid UTF-8.List(U8) gives the file's raw bytes, which works for any file, including binary files such as images.
It's a compile-time error if the file can't be found.
The dbg statement prints the value of an expression, for debugging purposes:
total = price * quantity
dbg total
When this runs, it prints something like [dbg] 42 to the program's standard error. The value is
printed using Str.inspect, so dbg works on values of any type which has an
inspect representation (including the to_inspect method described in
static dispatch).
dbg can be used anywhere statements can appear, including inside pure functions.
This is allowed because dbg output is only for the programmer, so program behavior never depends on it.
dbg statements that run during compile-time evaluation print their output when
the program is compiled (for example, during roc check or roc build), rather than when the compiled
program runs. dbg statements that run at runtime print their output each time they run.
dbg is intended for temporary debugging, so building an optimized program (which is what roc build
does by default) gives a warning for each dbg statement it contains.
The expect statement states that a Bool expression should evaluate to True.
It can be used for tests, and for checking assumptions at runtime.
Top-level expect
An expect at the top level of a module is a test:
double = |n| n * 2
expect double(21) == 42
Top-level expects are run by the roc test command, which reports how many passed and failed.
When one fails, the report shows the failing expression, along with the values of any top-level
names it referenced.
roc test runs the top-level expects in the given module and in every module it imports (including
modules in packages that are imported through a filesystem path, but not packages downloaded
from a URL, since those expects are the responsibility of the package's author).
Inside a top-level expect, the ? operator causes
the expect to fail if its expression evaluates to an Err, rather than returning early from a function.
If running a top-level expect reaches code that has a compilation error (such as a type
mismatch), roc test counts that expect as a compiler error rather than as passed or failed. This
applies even when the error is only reached indirectly, for example inside a function the expect
calls, or inside a method that == or a parser calls. An expect that calls a function
containing an error, but never reaches the part with the error, runs as usual. The error itself is
reported once. See Code With Errors.
Inline expect
An expect inside a function body checks an assumption each time that code runs:
withdraw = |balance, amount| {
expect amount <= balance
balance - amount
}
If an inline expect's condition evaluates to False, the failure is reported, and then
the program continues running as if the expect had not been there. (Unlike
crash, a failed expect does not stop the program.) Exactly how the failure is
reported is up to the platform.
Inline expects are for catching bugs during development, so they are omitted from optimized
builds (such as the default for roc build). As such, programs should never depend on
an expect running.
Since programs should never depend on an expect running, an expect cannot change where
the program goes next. These give a compile error when used directly inside an expect:
return, in both top-level and inline expectsbreak to exit a loop that's outside the expect, in both top-level and inline expects- the
? operator, in inline expects (in top-level expects, it makes the expect fail instead, as described above)
These rules don't apply inside a function defined within the expect, since leaving that
function doesn't leave the expect. Similarly, break is allowed inside a loop that's
written within the expect:
expect {
parse = |str| {
n = U64.from_str(str)? # Fine: returns early from `parse`, not from the `expect`
Ok(n * 2)
}
parse("21") == Ok(42)
}
For the same reason, an expect cannot change a variable declared outside of it. Reassigning
one inside an expect gives a compile error, but variables declared inside the expect can be
reassigned freely:
withdraw = |balance, amount| {
var $remaining = balance
expect {
$remaining = $remaining - amount # Error: `$remaining` was declared outside this `expect`
$remaining >= 0
}
expect {
var $left = balance
$left = $left - amount # Fine: `$left` was declared inside this `expect`
$left >= 0
}
$remaining - amount
}
The return statement immediately exits a function, returning the given value.
my_func = |arg| {
if arg == 0 {
return 0
# This line will never be reached.
}
arg - 1
}
The break statement immediately exits the innermost for or while loop. See loops for details.
(This has not been implemented yet. It will continue to the next iteration of a for or while loop.)
A crash statement crashes the running application. All code following the crash
becomes unreachable and will not be executed.
if some_condition {
crash "There is no way this program could possibly continue."
}
# This line will never be reached if `some_condition` was `True`
What happens after a crash is determined by the platform. Some may gracefully recover
and have some way of continuing the process, but others may terminate the process immediately.
A block statement is a group of statements which has its own scope, so
anything assigned in it can't be accessed outside the block.
It's different from a block expression in that
a block statement does not have an expression at the end. A common block
statement is one that does an early return in a conditional branch:
if foo {
…
} else {
bar = …
return bar
}
Having a single statement in a block statement is allowed:
if foo {
…
} else {
return bar
}