Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control Flow and Pattern Matching

Control flow in Ferlium is expression-based: if and match both produce values. This chapter explains how their types are determined and how to use them safely and clearly.

if is an expression

An if expression always produces a value. When both branches are present, the result is the value of the selected branch.

let x = if 1 < 2 { 10 } else { 20 };
x

The condition must be a bool. Both branches must produce compatible types, because the whole if has a single type.

if true { 1 } else { 2 }        // ok: int
if true { "yes" } else { "no" } // ok: string

Branch types must agree

If the branches have different types, the program does not type check:

if true { 1 } else { "no" }   // type error

if without else

An if without else is allowed when it is used for its effect. In that case, its result type is ().

let mut a = 0;
if true { a = 1 };
a

Trying to use a value-producing branch without an else is a type error:

// type error: missing else for a non-unit branch
fn f() { if true { 1 } }

match is an expression

A match expression selects one of several branches based on a value. Like if, it produces a value.

In this chapter, we only use literal patterns and the wildcard _.

let a = 0;
match a {
    0 => 1,
    1 => 2,
    _ => 3,
}

An entry LITERAL => EXPRESSION is called an arm.

All arms must have compatible types

Every arm in a match must produce a value of the same type, because the whole match has a single type:

match 0 {
    0 => 1,
    _ => 2,
}

This is a type error:

match 0 {
    0 => 1,
    _ => "no",
}

Exhaustiveness with _

When matching on literals, use _ as the default case unless you explicitly cover all possible values. For example, matching on bool can list both cases:

match true {
    true => 1,
    false => 0,
}

Early return

A function normally returns the value of its body block. Use return when you need to leave the function early:

fn clamp_non_negative(x: int) -> int {
    if x < 0 { return 0 };
    x
}

clamp_non_negative(-3)

The returned expression must have the function’s return type. A return exits the current function or lambda, even when it appears inside a nested block, if, or match arm.

let f = |x| {
    if x { return 1 };
    2
};

f(true)

Loop expressions

Use loop when the stopping condition lives inside the body. A break exits the loop, and continue skips to the next iteration.

let mut n = 0;
loop {
    n += 1;
    if n < 3 { continue };
    break n
}

A loop is an expression. Its result type comes from the values passed to break; a bare break is the same as break ().

let answer = loop {
    break 42
};

If a loop has no reachable break, its type is never.

Loop labels

Labels let break and continue target an outer loop directly:

let mut outer = 0;
let mut inner = 0;
'outer: loop {
    outer += 1;
    if outer == 3 { break inner };
    loop {
        inner += 1;
        continue 'outer
    }
}

The label is written before the target loop, followed by :, and reused after break or continue.

What comes next

Later chapters expand pattern matching to structured data and more powerful patterns. For now, you can use literals and _ to write clear, type-safe control flow.