8. Control Structures
Control structures allow you to control the flow of execution in your program with conditionals and loops.
8.1 Conditional Statements
If Statement
if (condition) {
// code block
}
If-Else Statement
if (condition) {
// code block
} else {
// code block
}
If-Else If-Else Statement
if (condition1) {
// code block
} else if (condition2) {
// code block
} else {
// code block
}
8.2 Loops
While Loop
while (condition) {
// code block
}
For Loop
The traditional for loop with initialization, condition, and increment:
for (var i = 0; i < 10; i = i + 1) {
// code block
}
For-In Loop
The for-in loop iterates over each element in an array:
for (var component in components) {
// code block - component is the current element
}
Example:
var numbers = [1, 2, 3, 4, 5];
for (var num in numbers) {
print(num); // Prints: 1, 2, 3, 4, 5
}
var names = ["Alice", "Bob", "Charlie"];
for (var name in names) {
print("Hello, " + name);
}
Notes:
- The loop variable is declared with
varand is scoped to the loop body - Each iteration assigns the current array element to the loop variable
- The collection must be an array - other types will throw a runtime error
breakandcontinuework the same as in other loops
Foreach Loop
The foreach loop is equivalent to the for-in loop: it iterates over each element in an array. Use whichever form you prefer.
foreach (var item in collection) {
// code block - item is the current element
}
Example:
var results = [10, 20, 30];
foreach (var x in results) {
print(x); // Prints: 10, 20, 30
}
The same rules as for-in apply: the loop variable is declared with var, the collection must be an array, and break and continue work as in other loops.
Break and Continue
while (true) {
if (condition) {
break; // Exit loop
}
if (skipCondition) {
continue; // Skip to next iteration
}
}
8.3 Pattern Matching
Pattern matching allows you to match values against patterns and execute code based on the match. It's similar to switch statements in other languages but more powerful, supporting pattern matching on arrays, objects, and nested structures.
Match Expression
The match expression evaluates a value against a series of patterns and executes the code for the first matching pattern:
var result = match value {
case pattern1: expression1;
case pattern2: expression2;
default: defaultExpression;
};
Example:
var x = 42;
var result = match x {
case 42: "matched 42";
case 10: "matched 10";
default: "no match";
};
print(result);
Match expressions can also be used as statements (without assigning to a variable):
match x {
case 42: print("matched 42");
default: print("no match");
}
Block Bodies and Multiple Statements
A case body can be a single expression or a block { ... } with multiple statements. This allows side effects (e.g. print, assignments) before producing the case value.
Last expression wins (match case bodies): When a match is used as an expression, a case block's value is the last statement's value if that statement is an expression. If the last statement is not an expression (e.g. print(...)), the match yields null. The same last expression wins rule also applies to functions, methods, and lambdas (see 9. Functions). Blocks in if, while, and for do not produce a value; they are purely imperative.
var result = match x {
case 42: {
print("side effect");
"result"; // last expression is the case value
}
default: "no match";
};
print(result); // Prints: "side effect" then "result"
var result = match x {
case 42: {
print("only side effect");
}
default: "no match";
};
print(result); // result is null (block ends with statement, not expression)
Summary:
- Single expression:
case 42: "value";— the expression is the case value. - Block with expression last:
case 42: { stmt; expr; }— the last expression is the case value. - Block with statement last:
case 42: { print(x); }— the case value isnull.
Pattern Types
Variant Patterns
For values built from sum types, use a variant pattern: ConstructorName(binding, ...). The value must be that variant and have the same number of payload slots; payloads are bound to the names in the pattern. A bare constructor name (case Ok: / case None:) is the same match with implicit _ for each payload — equivalent to case Ok(): for a unit variant. It does not bind a catch-all variable of that name.
type Result = Ok(value) | Err(message);
var r = divide(10, 0);
match r {
case Ok(v): print("value: " + v);
case Err(msg): print("error: " + msg);
}
type Result = Ok() | Err(message);
var r = Err("ciao");
var m3 = match r {
case Ok: "ok: ";
case Err(msg): "error: " + msg;
};
print(m3);
Literal Patterns
Match exact values (numbers, strings, booleans, null):
match x {
case 42: "number";
case "hello": "string";
case true: "boolean";
case null: "null";
}
Identifier Patterns
Bind the matched value to a variable name. If the identifier is a declared variant constructor, it is a variant pattern instead (see above), not a catch-all:
match x {
case y: y + 10; // Binds x to y, then evaluates y + 10
}
Wildcard Pattern
Match anything without binding (use _):
match x {
case 10: "ten";
case _: "other"; // Matches anything else
}
Array Patterns
Match arrays by structure and extract elements:
var arr = [1, 2, 3];
match arr {
case [1, 2, 3]: "exact match";
case [x, y, z]: "three elements: " + x + ", " + y + ", " + z;
case [first, ...rest]: "first is " + first + ", rest has " + length(rest) + " elements";
default: "no match";
}
Rest Pattern: Use ...rest to capture remaining array elements:
var arr = [1, 2, 3, 4, 5];
match arr {
case [first, second, ...rest]:
// first = 1, second = 2, rest = [3, 4, 5]
first + second + length(rest);
}
Notes:
- Array patterns must match the exact length (unless using rest pattern)
- The rest pattern (
...rest) must be at the end of the array pattern - You can use nested patterns within array patterns
Object Patterns
Match objects by properties and extract values:
var obj = { name: "Alice", age: 30 };
match obj {
case { name, age }: name + " is " + age; // Shorthand: binds name and age
case { name: userName, age: userAge }: userName + " is " + userAge; // Explicit binding
default: "unknown";
}
Nested Object Patterns:
var obj = { user: { name: "Bob", age: 25 }, role: "admin" };
match obj {
case { user: { name, age }, role }:
name + " (" + age + ") is " + role;
}
Notes:
- Object patterns match if all specified properties exist
- Shorthand
{ name }is equivalent to{ name: name } - You can use nested patterns to match nested objects
- Missing properties cause the pattern to not match
Guards
An optional if condition after the pattern gates the case. The pattern is tried first; bindings from the pattern are in scope for the condition. If the condition is false, the next case is tried. This is the same if word as guarded catch (e if …).
var n = 3;
var result = match n {
case x if x > 10: "big";
case x: "small";
};
print(result);
Under --strict-types, a guarded arm does not count as covering a variant or as a catch-all: case Ok(v) if v > 0 does not exhaust Ok, and case x if … is not a default.
Default Case
The default case is optional and matches when no other pattern matches:
match x {
case 1: "one";
case 2: "two";
default: "other"; // Matches anything else
}
If no pattern matches and there's no default case, a runtime error is thrown.
Pattern Matching in Practice
Pattern matching is particularly useful for handling different message types in actors or processing structured data:
// Handle different message types
var msg = receive();
match msg {
case { type: "Start", payload: p }: handleStart(p);
case { type: "Stop" }: handleStop();
case { type: "Update", id: id, value: v }: handleUpdate(id, v);
default: print("Unknown message type");
}
// Process API responses
var response = fetchData();
match response {
case { status: 200, data: d }: processData(d);
case { status: 404 }: print("Not found");
case { status: 500, error: e }: print("Server error: " + e);
default: print("Unexpected response");
}
- Patterns are matched in order - the first matching pattern whose optional
ifguard succeeds executes - Identifier patterns always match unless the name is a declared variant constructor (
case Ok:matches theOkvariant, not everything) - A failed
ifguard tries the next case; anifinside the case body does not - Use wildcard patterns (
_) when you don't need the value - Nested patterns allow deep matching of complex data structures
- Variables bound in patterns are scoped to the case body
8.4 Exception Handling
Throw Statement
The throw statement throws an exception. The expression can be any value (string, object, etc.).
throw expression;
Example:
throw "Error occurred";
throw "Division by zero";
Try-Catch-Finally
Exception handling allows you to catch and handle errors gracefully.
try {
// statements that may throw
} catch (exceptionVar if condition) {
// handle when condition is true (guarded catch)
} catch (exceptionVar) {
// fallback handler
} finally {
// cleanup code (always executes)
}
- try block: Contains code that may throw an exception
- catch clause: Handles exceptions. The exception variable is optional - if provided, it contains the exception value
- finally block: Optional cleanup code that always executes, whether an exception occurred or not
Examples:
// Basic try-catch
try {
var result = 10 / 0;
} catch (error) {
print("Error: " + error);
}
// Tagged catch (throw dict with kind/message for e.kind filters)
try {
throw dict { "kind": "IO", "message": "disk full" };
} catch (e if e.kind == "IO") {
print("IO: " + e.message);
} catch (e) {
print("Other: " + e);
}
// For expected success/failure in a return value, prefer result / option
// instead of throw — see Built-in Functions 12.19.1.
// Try-catch-finally
try {
// risky operation
} catch (error) {
print("Caught: " + error);
} finally {
print("Cleanup always runs");
}
// Multiple catch clauses (currently only the first one executes)
try {
// code
} catch (error) {
// This catch clause will catch any exception
// handle error
} catch (otherError) {
// This catch clause is unreachable - the first one catches all exceptions
// Note: Exception type matching may be added in future versions
} finally {
// cleanup
}
Important Notes:
- The finally block always executes, whether an exception occurred or not
- Exceptions can be any MALDA value (string, number, object, etc.)
- Multiple catch clauses: Currently, all catch clauses match any exception type. The first catch clause will always catch the exception, making subsequent catch clauses unreachable. Exception type matching may be added in future versions.
- If no catch clause is present and an exception occurs, it propagates up after the finally block executes
8.5 Deterministic Cleanup
Besides finally, MALDA offers two dedicated forms for releasing resources: defer registers cleanup next to the code that acquired the resource, and the using resource block ties a resource's lifetime to a scope.
defer
A defer block is registered when execution reaches it and runs when the enclosing block or function exits — on normal completion, on return, and on an exception alike:
function process(path) {
var handle = openHandle(path);
defer {
closeHandle(handle);
}
// Any exit path below still closes the handle
if (not isValid(handle)) {
return null;
}
return readAll(handle);
}
Several deferred blocks in the same scope run in reverse registration order (last registered, first executed), which lets you unwind acquisitions in the opposite order to how you made them:
function demo() {
defer { print("first registered, runs last"); }
defer { print("second registered, runs first"); }
print("body");
}
demo();
// body
// second registered, runs first
// first registered, runs last
Rules to keep in mind:
deferrequires an enclosing block, function, orusingbody. At top level with no enclosing scope it raises 'defer' is only valid inside a block, function, or 'using' body.- A
deferstatement that is never reached is never registered, so cleanup only happens for resources actually acquired. - An error thrown inside a deferred block is swallowed, so a failing cleanup cannot mask the exception that is already unwinding. Handle errors explicitly inside the deferred block if you need to observe them.
return,break, andcontinueinside a deferred block do propagate, so avoid them unless you intend to change the control flow of the enclosing scope.
using Resource Blocks
The using name = expression { ... } form binds a resource for the duration of a block and disposes it when the block exits, including on exceptions:
using conn = openConnection(url) {
var rows = conn.query("SELECT 1");
print(rows.length);
}
// conn has been disposed here
On exit MALDA looks for a cleanup method on the resource object and calls the first one it finds, in this order:
dispose()close()disconnect()
If the resource is null, or is not an object, or defines none of those methods, the block still runs and no cleanup is attempted. The bound name is scoped to the block and is not visible afterwards.
class TempFile {
public var path;
function TempFile(path) {
this.path = path;
}
public function close() {
deleteFile(path);
print("removed " + path);
}
}
using tmp = new TempFile("scratch.txt") {
writeFile(tmp.path, "work in progress");
}
// removed scratch.txt
using block when the resource has a clear disposal method and a clear scope. Use defer when cleanup is not a single method call, when several resources are acquired conditionally, or when the cleanup belongs to a whole function rather than to a nested block. Note that this using is unrelated to the top-level using that aliases a package.
Both forms are supported by the interpreter, the C# transpiler, and the JavaScript transpiler.
See Also
- 7. Expressions - Expressions used in conditions
- 5. Variables - Variable scope in control structures and destructuring