Skip to content

Control Flow

This part covers how to control the flow of execution in Dovetail programs.


In Dovetail, if is an expression - it always returns a value. This makes it more powerful than traditional if statements.

if condition then
expression1
else
expression2

Simple if/else:

let max = if a > b then a else b

Multi-line blocks:

let result = if user.isActive then
debug("User is active")
user.score * 2
else
debug("User is inactive")
0

Single-line form:

let sign = if x > 0 then "positive" else "non-positive"

Use else if for multiple conditions:

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

When you don’t need a value, you can omit else. The expression returns Unit:

if shouldLog then
debug("Logging enabled")

If expressions can be nested:

let category = if age < 13 then
"child"
else
if age < 20 then "teenager" else "adult"

But prefer else if chains or match for better readability.


match is Dovetail’s powerful pattern matching construct. It’s like switch in other languages, but much more expressive.

match value with
case pattern1 => result1
case pattern2 => result2
case _ => defaultResult
let dayName = match dayNumber with
case 1 => "Monday"
case 2 => "Tuesday"
case 3 => "Wednesday"
case 4 => "Thursday"
case 5 => "Friday"
case 6 => "Saturday"
case 7 => "Sunday"
case _ => "Invalid day"

Pattern matching shines with enums:

enum Color =
Red
Green
Blue
RGB(Uint8, Uint8, Uint8)
let description = match color with
case Red => "Pure red"
case Green => "Pure green"
case Blue => "Pure blue"
case RGB(r, g, b) => "RGB($r, $g, $b)"
let message = match maybeUser with
case Some(user) => "Hello, ${user.name}!"
case None => "Hello, guest!"
match fetchData() with
case Ok(data) => processData(data)
case Error(err) => debug("Error: $err")
record Point =
x: Int32
y: Int32
let quadrant = match point with
case Point { x; y } if x > 0 and y > 0 => "Q1"
case Point { x; y } if x < 0 and y > 0 => "Q2"
case Point { x; y } if x < 0 and y < 0 => "Q3"
case Point { x; y } if x > 0 and y < 0 => "Q4"
case _ => "Origin or on axis"

Commas are also accepted between record-pattern fields, but dovetail fmt normalizes them to semicolons. In a multiline pattern, semicolons appear between fields but not after the last field.

let description = match (x, y) with
case (0, 0) => "Origin"
case (0, _) => "On Y axis"
case (_, 0) => "On X axis"
case (a, b) if a == b => "On diagonal"
case _ => "Somewhere else"

A list matches either as a fixed set of elements or as a head and a tail:

let description = match xs with
case [] => "empty"
case [only] => "exactly one"
case [a, b] => "exactly two"
case h :: t => "at least one more"

h :: t binds h to the first element and t to the rest, which makes recursion over a list read directly:

function sum(xs: List<Int32>): Int32 =
match xs with
case [] => 0
case h :: t => h + sum(t)

[] and h :: t together cover every list, so the pair needs no wildcard:

let first = match xs with
case [] => "none"
case h :: t => h.format()

Fixed-length patterns do not cover everything, though — case [] plus case [a, b] leaves a one-element list unmatched, and the compiler says so:

non-exhaustive match: missing case for [_]

These patterns match a List only. Arrays have no pattern form; index them or iterate them instead.

Add if after a pattern for additional conditions:

let category = match age with
case n if n < 0 => "Invalid"
case n if n < 13 => "Child"
case n if n < 20 => "Teenager"
case n if n < 65 => "Adult"
case _ => "Senior"

Use _ to match anything you don’t care about:

match triple with
case (first, _, _) => first // Only care about first element

Patterns can bind values to names:

match result with
case Ok(value) => debug("Got: $value")
case Error(e) => debug("Error: $e")

The compiler ensures all cases are covered. This won’t compile:

enum Direction = North | South | East | West
// Error: non-exhaustive match
let name = match dir with
case North => "N"
case South => "S"
// Missing East and West!

Use _ as a catch-all if needed:

let name = match dir with
case North => "N"
case South => "S"
case _ => "Other"

Dovetail provides for and while loops for iteration.

Iterate over a value that implements Iterable<T>, including arrays, lists, slices, and collection ranges:

for item in list do
debug(item)

Over a slice:

let values = [|10, 20, 30, 40|]
let mutable total = 0
for value in values[|1..3|] do
total = total + value
assert total == 50

See Lists, Arrays, and Slices for slicing forms and shared-mutation behavior. The .. notation is only valid inside slicing brackets; use the collection Range API for a numeric loop.

With index using enumerate:

for (index, item) in list.zipWithIndex() do
debug("$index: $item")

Over a range:

for i in Range.new(0, 10) do
debug(i) // 0 to 9
for i in Range.inclusive(0, 10) do
debug(i) // 0 to 10 (inclusive)

Iterating over maps:

for (key, value) in map do
debug("$key -> $value")

Execute while a condition is true:

let mutable count = 0
while count < 10 do
debug(count)
count = count + 1

Loops return Unit:

let result: Unit = for i in [|0, 1, 2, 3, 4|] do
debug(i)

Use break to exit a loop early:

let mutable found = false
for item in list do
if item == target then
found = true
break

Use continue to skip to the next iteration:

for item in list do
if item < 0 then
continue // Skip negative numbers
debug(item)

For many use cases, functional methods are cleaner than loops:

// Instead of a loop to transform
let doubled = numbers.map(x => x * 2)
// Instead of a loop to filter
let positive = numbers.filter(x => x > 0)
// Instead of a loop to sum
let total = numbers.fold(0, (acc, x) => acc + x)
// Instead of a loop to find
let found = numbers.find(x => x > 100)

These functional approaches are often more readable and eliminate the need for mutable variables.


Dovetail provides expressive control flow constructs:

  • If expressions - Always return a value, can be used anywhere an expression is expected
  • Match expressions - Powerful pattern matching with destructuring, guards, and exhaustiveness checking
  • Loops - for and while for iteration, with break and continue for control

A key principle: prefer pattern matching over long if/else chains, and prefer functional methods (map, filter, fold) over imperative loops when it makes the code clearer.

In the next part, we’ll explore functions in depth.

For restrictions on loops that suspend, see Async Programming.