8. Strutture di controllo
Le strutture di controllo permettono di governare il flusso di esecuzione del programma con condizionali e cicli.
8.1 Istruzioni condizionali
Istruzione if
if (condition) {
// code block
}
Istruzione if-else
if (condition) {
// code block
} else {
// code block
}
Istruzione if-else if-else
if (condition1) {
// code block
} else if (condition2) {
// code block
} else {
// code block
}
8.2 Cicli
Ciclo while
while (condition) {
// code block
}
Ciclo for
Il ciclo for tradizionale con inizializzazione, condizione e incremento:
for (var i = 0; i < 10; i = i + 1) {
// code block
}
Ciclo for-in
Il ciclo for-in itera su ogni elemento di un array:
for (var component in components) {
// code block - component is the current element
}
Esempio:
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);
}
Note:
- La variabile di ciclo si dichiara con
vare ha scope nel corpo del ciclo - Ogni iterazione assegna l'elemento corrente dell'array alla variabile di ciclo
- La collezione deve essere un array: altri tipi sollevano un errore a runtime
breakecontinuefunzionano come negli altri cicli
Ciclo foreach
Il ciclo foreach equivale al ciclo for-in: itera su ogni elemento di un array. Usa la forma che preferisci.
foreach (var item in collection) {
// code block - item is the current element
}
Esempio:
var results = [10, 20, 30];
foreach (var x in results) {
print(x); // Prints: 10, 20, 30
}
Valgono le stesse regole del for-in: la variabile di ciclo si dichiara con var, la collezione deve essere un array, e break e continue funzionano come negli altri cicli.
Break e continue
while (true) {
if (condition) {
break; // Exit loop
}
if (skipCondition) {
continue; // Skip to next iteration
}
}
8.3 Pattern matching
Il pattern matching permette di confrontare valori con pattern ed eseguire codice in base alla corrispondenza. È simile alle istruzioni switch di altri linguaggi, ma più potente: supporta il matching su array, oggetti e strutture nidificate.
Espressione match
L'espressione match valuta un valore rispetto a una serie di pattern ed esegue il codice del primo pattern che corrisponde:
var result = match value {
case pattern1: expression1;
case pattern2: expression2;
default: defaultExpression;
};
Esempio:
var x = 42;
var result = match x {
case 42: "matched 42";
case 10: "matched 10";
default: "no match";
};
print(result);
Le espressioni match si possono usare anche come istruzioni (senza assegnarle a una variabile):
match x {
case 42: print("matched 42");
default: print("no match");
}
Corpi a blocco e istruzioni multiple
Il corpo di un case può essere una singola espressione oppure un blocco { ... } con più istruzioni. Questo permette effetti collaterali (per esempio print, assegnamenti) prima di produrre il valore del case.
Vince l'ultima espressione (corpi dei case di match): quando un match è usato come espressione, il valore di un blocco case è il valore dell'ultima istruzione se quella istruzione è un'espressione. Se l'ultima istruzione non è un'espressione (per esempio print(...)), il match produce null. La stessa regola vince l'ultima espressione vale anche per funzioni, metodi e lambda (vedi 9. Funzioni). I blocchi in if, while e for non producono un valore: sono puramente imperativi.
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)
Riepilogo:
- Espressione singola:
case 42: "value";— l'espressione è il valore del case. - Blocco con espressione in fondo:
case 42: { stmt; expr; }— l'ultima espressione è il valore del case. - Blocco con istruzione in fondo:
case 42: { print(x); }— il valore del case ènull.
Tipi di pattern
Pattern di variante
Per i valori costruiti da sum type, usa un pattern di variante: ConstructorName(binding, ...). Il valore deve essere quella variante e avere lo stesso numero di slot di payload; i payload vengono legati ai nomi nel pattern. Un nome di costruttore nudo (case Ok: / case None:) è lo stesso match con _ implicito per ogni payload — equivalente a case Ok(): per una variante senza argomenti. Non lega una variabile catch-all con quel nome.
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);
Pattern letterali
Corrispondono a valori esatti (numeri, stringhe, booleani, null):
match x {
case 42: "number";
case "hello": "string";
case true: "boolean";
case null: "null";
}
Pattern identificatore
Legano il valore corrispondente a un nome di variabile. Se l'identificatore è un costruttore di variante dichiarato, è un pattern di variante (vedi sopra), non un catch-all:
match x {
case y: y + 10; // Binds x to y, then evaluates y + 10
}
Pattern wildcard
Corrispondono a qualsiasi cosa senza fare binding (usa _):
match x {
case 10: "ten";
case _: "other"; // Matches anything else
}
Pattern di array
Corrispondono agli array per struttura ed estraggono gli elementi:
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";
}
Pattern rest: usa ...rest per catturare gli elementi rimanenti dell'array:
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);
}
Note:
- I pattern di array devono corrispondere alla lunghezza esatta (a meno che non usi il pattern rest)
- Il pattern rest (
...rest) deve stare in fondo al pattern di array - Puoi usare pattern nidificati dentro i pattern di array
Pattern di oggetto
Corrispondono agli oggetti per proprietà ed estraggono i valori:
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";
}
Pattern di oggetto nidificati:
var obj = { user: { name: "Bob", age: 25 }, role: "admin" };
match obj {
case { user: { name, age }, role }:
name + " (" + age + ") is " + role;
}
Note:
- I pattern di oggetto corrispondono se esistono tutte le proprietà specificate
- La forma breve
{ name }equivale a{ name: name } - Puoi usare pattern nidificati per corrispondere a oggetti nidificati
- Le proprietà mancanti fanno sì che il pattern non corrisponda
Guardie
Una condizione if opzionale dopo il pattern filtra il case. Prima si prova il pattern; i binding del pattern sono visibili nella condizione. Se la condizione è falsa, si prova il case successivo. È la stessa parola if di catch (e if …).
var n = 3;
var result = match n {
case x if x > 10: "big";
case x: "small";
};
print(result);
Con --strict-types, un braccio con guardia non copre una variant e non vale come catch-all: case Ok(v) if v > 0 non esaurisce Ok, e case x if … non è un default.
Case default
Il case default è opzionale e corrisponde quando nessun altro pattern corrisponde:
match x {
case 1: "one";
case 2: "two";
default: "other"; // Matches anything else
}
Se nessun pattern corrisponde e non c'è un case default, viene sollevato un errore a runtime.
Pattern matching in pratica
Il pattern matching è particolarmente utile per gestire tipi di messaggio diversi negli actor o per elaborare dati strutturati:
// 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");
}
- I pattern vengono confrontati in ordine: viene eseguito il primo che corrisponde e la cui guardia
ifopzionale ha successo - I pattern identificatore corrispondono sempre, a meno che il nome sia un costruttore di variante dichiarato (
case Ok:corrisponde alla varianteOk, non a tutto) - Una guardia
iffallita prova il case successivo; unifdentro il corpo del case no - Usa i pattern wildcard (
_) quando non ti serve il valore - I pattern nidificati permettono il matching in profondità di strutture dati complesse
- Le variabili legate nei pattern hanno scope nel corpo del case
8.4 Gestione delle eccezioni
Istruzione throw
L'istruzione throw solleva un'eccezione. L'espressione può essere qualsiasi valore (stringa, oggetto, ecc.).
throw expression;
Esempio:
throw "Error occurred";
throw "Division by zero";
Try-catch-finally
La gestione delle eccezioni permette di catturare e trattare gli errori in modo controllato.
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)
}
- blocco try: contiene il codice che può sollevare un'eccezione
- clausola catch: gestisce le eccezioni. La variabile di eccezione è opzionale: se presente, contiene il valore dell'eccezione
- blocco finally: codice di cleanup opzionale che viene sempre eseguito, che si sia verificata un'eccezione o no
Esempi:
// 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
}
Note importanti:
- Il blocco finally viene sempre eseguito, che si sia verificata un'eccezione o no
- Le eccezioni possono essere qualsiasi valore MALDA (stringa, numero, oggetto, ecc.)
- Più clausole catch: al momento tutte le clausole catch corrispondono a qualsiasi tipo di eccezione. La prima clausola catch cattura sempre l'eccezione, rendendo irraggiungibili le successive. Il matching per tipo di eccezione potrebbe arrivare in versioni future.
- Se non c'è alcuna clausola catch e si verifica un'eccezione, questa si propaga verso l'alto dopo l'esecuzione del blocco finally
8.5 Cleanup deterministico
Oltre a finally, MALDA offre due forme dedicate per rilasciare le risorse: defer registra il cleanup accanto al codice che ha acquisito la risorsa, e il blocco risorsa using lega il ciclo di vita di una risorsa a uno scope.
defer
Un blocco defer viene registrato quando l'esecuzione lo raggiunge e gira all'uscita del blocco o della funzione che lo racchiude — al completamento normale, su return e su un'eccezione allo stesso modo:
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);
}
Più blocchi deferred nello stesso scope girano in ordine inverso di registrazione (ultimo registrato, primo eseguito), così puoi smontare le acquisizioni nell'ordine opposto a quello in cui le hai fatte:
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
Regole da tenere a mente:
deferrichiede un blocco, una funzione o un corpousingcircostante. A top level senza uno scope che lo racchiude solleva 'defer' is only valid inside a block, function, or 'using' body.- Un'istruzione
deferche non viene mai raggiunta non viene mai registrata, quindi il cleanup avviene solo per le risorse effettivamente acquisite. - Un errore sollevato dentro un blocco deferred viene inghiottito, così un cleanup che fallisce non può mascherare l'eccezione già in unwinding. Gestisci gli errori esplicitamente dentro il blocco deferred se ti serve osservarli.
return,breakecontinuedentro un blocco deferred si propagano, quindi evitali a meno che tu non intenda cambiare il flusso di controllo dello scope circostante.
Blocchi risorsa using
La forma using name = expression { ... } lega una risorsa per la durata di un blocco e ne esegue il dispose all'uscita del blocco, anche in caso di eccezioni:
using conn = openConnection(url) {
var rows = conn.query("SELECT 1");
print(rows.length);
}
// conn has been disposed here
All'uscita MALDA cerca un metodo di cleanup sull'oggetto risorsa e chiama il primo che trova, in questo ordine:
dispose()close()disconnect()
Se la risorsa è null, o non è un oggetto, o non definisce nessuno di quei metodi, il blocco gira comunque e non viene tentato alcun cleanup. Il nome legato ha scope nel blocco e non è visibile dopo.
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 quando la risorsa ha un metodo di dispose chiaro e uno scope chiaro. Usa defer quando il cleanup non è una singola chiamata di metodo, quando più risorse vengono acquisite in modo condizionale, o quando il cleanup appartiene a un'intera funzione piuttosto che a un blocco nidificato. Nota che questo using non ha a che fare con il using a top level che crea un alias di pacchetto.
Entrambe le forme sono supportate dall'interprete, dal transpiler C# e dal transpiler JavaScript.
Vedi anche
- 7. Espressioni - Espressioni usate nelle condizioni
- 5. Variabili - Scope delle variabili nelle strutture di controllo e destrutturazione