Skip to main content

Control flow

Mochi has a small set of control-flow constructs: if for conditionals, for and while for loops, and match for pattern matching. if also works as an expression, so short branches read cleanly without intermediate variables.

if, else if, else​

The statement form has a brace body for each branch:

if temperature < 0 {
print("freezing")
} else if temperature < 20 {
print("mild")
} else {
print("warm")
}

The condition must have type bool. Mochi has no implicit truthiness, so if 0 is a compile-time error. Convert numbers and strings explicitly:

if items.len() > 0 { ... } // ok
if items.len() { ... } // error: condition is not bool

Branches are blocks. Each block has its own scope. let bindings declared inside one branch are not visible outside.

if as an expression​

In a value position, if returns the value of the chosen branch. Use then and else:

let label = if score >= 50 then "pass" else "fail"

Both branches must produce a value of the same type (or types that combine into a known union). The expression form chains naturally:

let grade =
if score >= 90 then "A"
else if score >= 80 then "B"
else if score >= 70 then "C"
else "F"

For longer expression-form branches, wrap each branch in { ... } and end with the value:

let summary = if user.is_admin {
let role = "administrator"
let last = user.last_login
role + " (last seen " + str(last) + ")"
} else {
"regular user"
}

for loops​

The general form is for x in iterable:

for i in 1..4 {
print(i) // 1, 2, 3
}

for x in [10, 20, 30] {
print(x)
}

for ch in "hello" {
print(ch)
}

for (k, v) in {"a": 1, "b": 2} {
print(k, v)
}

Ranges​

a..b produces a half-open range from a (inclusive) to b (exclusive). a..=b is inclusive on both ends.

for i in 0..3 { print(i) } // 0, 1, 2
for i in 0..=3 { print(i) } // 0, 1, 2, 3

Ranges are values. Store them in a binding or pass them to any function that takes an iterable.

break and continue​

break exits the loop. continue skips to the next iteration.

for n in 1..20 {
if n % 7 == 0 { break }
if n % 2 == 0 { continue }
print(n)
}

while loops​

var n = 0
while n < 5 {
print(n)
n = n + 1
}

The condition must be bool and is evaluated before each iteration. Mochi has no do-while. To run the body once before checking, place the body and condition explicitly:

var attempt = 0
while true {
if try_fetch() { break }
attempt = attempt + 1
if attempt >= 3 { break }
}

break and continue work the same as in for.

match expressions​

match performs pattern matching across literal values, type narrowings, and union variants. Each arm is checked in order; the first match wins.

fun classify(n: int): string {
return match n {
0 => "zero",
1 => "one",
n if n < 0 => "negative",
_ => "many"
}
}

Arms separate the pattern from the body with =>. A guard (if ...) optionally constrains the match. _ is the wildcard.

Patterns​

PatternMatches
42The literal 42.
"abc"The literal string.
nameAny value, binding it to name.
_Any value, discarded.
VariantA no-arg union variant.
Variant(x, y)A union variant, binding the constructor arguments.
[x, y, ...rest]A list with at least two elements, head bound.
{"key": v}A map with the given key, value bound.
n: intA value with type narrowed to int.

Pattern matching on unions​

type Tree =
Leaf
| Node(value: int, left: Tree, right: Tree)

fun sum(t: Tree): int {
return match t {
Leaf => 0,
Node(v, l, r) => v + sum(l) + sum(r)
}
}

The exhaustiveness checker warns when a variant is missing. Suppress the warning with a _ => ... catch-all when intentional.

Pattern matching on optional values​

fun greet(name: string | nil): string {
return match name {
nil => "anonymous",
n => "hello, " + n
}
}

Guards​

match score {
s if s >= 90 => print("A"),
s if s >= 80 => print("B"),
_ => print("C or below")
}

Guards are arbitrary boolean expressions. They run only when the structural pattern already matches.

Control flow with collections​

for over a list pairs naturally with from / where / select. The query form is often cleanest, with a loop for side effects:

let unread = from b in books where !b.read select b
for b in unread {
print(b.title)
}

Read more in datasets.

Short-circuit evaluation​

&& and || are short-circuiting. The right operand is evaluated only when needed.

if user != nil && user.is_admin {
...
}

This is the idiomatic pattern for combining an existence check with a follow-on property access.

Common errors​

MessageCauseFix
condition is not boolif or while with a non-boolUse an explicit comparison.
match is not exhaustiveMissing variant or fall-throughAdd a branch or _ => ....
unreachable code after matchA previous arm covers everythingRemove the trailing branch.
break outside loopbreak in a match armUse return or restructure.

Next​

  • Functions: closures, defaults, varargs
  • Types: structs, unions, methods
  • Errors: optional values, expect, panic
  • Datasets: from / where / select