Manuale di riferimento MALDA™

Il linguaggio di programmazione AI-First - Versione 1.0.11

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:

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:

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:

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:

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");
}
Consigli sul pattern matching:

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)
}

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:

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:

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:

  1. dispose()
  2. close()
  3. 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
Come scegliere: usa un blocco 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