Manuale di riferimento MALDA™

Il linguaggio di programmazione AI-First - Versione 1.0.11

17. Actor

MALDA offre il supporto nativo al modello ad actor per la programmazione concorrente. Gli actor sono unità di computazione indipendenti e isolate che comunicano esclusivamente tramite message passing asincrono. Ogni actor ha il proprio stato isolato e processa i messaggi in sequenza, garantendo una programmazione concorrente thread-safe senza meccanismi di locking espliciti.

17.1 Dichiarazione degli actor

Gli actor si dichiarano con la keyword actor, in modo analogo alle dichiarazioni di classe:

actor ActorName {
    // Fields (state)
    var field1 = initialValue;
    var field2;
    
    // Constructor (optional)
    function ActorName(param1, param2) {
        field1 = param1;
        field2 = param2;
    }
    
    // Message handlers
    on messageHandler1() {
        // Handle message
    }
    
    on messageHandler2(param) {
        // Handle message with parameter
    }
    
    // Default handler (optional)
    on handle(msg) {
        // Handles any message that doesn't match a specific handler
    }
}

Componenti di un actor

17.2 Spawn degli actor

Gli actor si istanziano con l'espressione spawn:

var actorRef = spawn ActorName(arg1, arg2);

L'espressione spawn:

Esempio

actor Counter {
    var count = 0;
    
    on increment() {
        count = count + 1;
    }
    
    on get() {
        print(count);
    }
}

var counter = spawn Counter();

17.3 Invio dei messaggi

I messaggi si inviano agli actor con l'istruzione send nella sintassi call-style:

// Call-style syntax
send actorRef.handlerName(arg1, arg2);

// Call-style with callback
send actorRef.handlerName(arg1) then (result) {
    // Handle reply value
};

Sintassi di invio dei messaggi

Esempio

actor Greeter {
    var name;
    
    function Greeter(actorName) {
        name = actorName;
    }
    
    on greet() {
        print($"Hello from {name}!");
    }
    
    on greetWith(message) {
        print($"{name}: {message}");
    }
}

var greeter = spawn Greeter("Alice");
send greeter.greet();                       // Calls greet() handler
send greeter.greetWith("Hello, World!");    // Calls greetWith() handler with message
send greeter("Hello!");                    // Calls handle() handler (if defined)

Dettagli del message passing

Request/Response con callback

Puoi inviare un messaggio e registrare un callback che verrà eseguito quando il destinatario risponde:

actor Worker {
    on compute(value) {
        var result = value * 2;
        reply(result);  // Sends a reply back to the sender
    }
}

actor Coordinator {
    on start() {
        var worker = spawn Worker();

        send worker.compute(21) then (result) {
            print($"Coordinator: received result = {result}");
        };
    }
}

var coordinator = spawn Coordinator();

// Small delay to ensure actor loop has started
sleep(100);

send coordinator.start();

// Give actors time to process messages and callbacks
sleep(500);

17.4 Message handler

I message handler sono funzioni che processano i messaggi in arrivo. Si dichiarano con la keyword on:

actor MyActor {
    // Handler with no parameters
    on handlerName() {
        // Process message
    }
    
    // Handler with parameters
    on handlerWithParams(param1, param2) {
        // Process message with parameters
    }
    
    // Default handler (receives any unmatched message)
    on handle(msg) {
        // Process any message
    }
}

Risoluzione degli handler

Quando viene inviato un messaggio, il runtime determina quale handler chiamare con le seguenti regole:

  1. Se viene fornito un nome di handler esplicito (tramite send target.handlerName(...)), viene usato quel nome di handler
  2. Altrimenti, se il payload del messaggio è una stringa, viene usato come nome dell'handler
  3. Altrimenti, viene usato l'handler handle come default
  4. Il runtime cerca un handler con quel nome
  5. Se non viene trovato un handler specifico, l'handler handle viene usato come fallback (se definito)
  6. Se non viene trovato alcun handler, viene sollevato un errore di runtime

Esempi:

actor MyActor {
    on handle(msg) {
        print("Default handler: " + msg);
    }
    
    on process(data) {
        print("Process handler: " + data);
    }
}

var actor = spawn MyActor();

// Explicit handler name - calls process()
send actor.process(123);

// No handler name, non-string payload - calls handle()
send actor(456);

// No handler name, string payload - uses string as handler name, calls process()
send actor("process");

Parametri degli handler

17.5 Isolamento dello stato degli actor

Ogni istanza di actor ha uno stato completamente isolato. I campi dichiarati in un actor sono privati di quella istanza:

actor BankAccount {
    var balance = 0;
    var accountName;
    
    function BankAccount(name) {
        accountName = name;
    }
    
    on deposit(amount) {
        balance = balance + amount;
    }
    
    on withdraw(amount) {
        if (balance >= amount) {
            balance = balance - amount;
        }
    }
}

// Each instance has its own isolated state
var account1 = spawn BankAccount("Alice");
var account2 = spawn BankAccount("Bob");

send account1.deposit(100);  // account1.balance = 100
send account2.deposit(200);  // account2.balance = 200 (independent)

17.6 Riferimento self

Dentro i message handler di un actor, la keyword self fornisce un riferimento all'istanza corrente dell'actor:

actor PingPong {
    var partner;
    
    on setPartner(other) {
        partner = other;
    }
    
    on ping() {
        print("Received ping");
        if (partner != null) {
            send partner.pong(self);  // Send self reference
        }
    }
    
    on pong(sender) {
        print("Received pong");
        if (sender != null) {
            send sender.ping();
        }
    }
}

var ping = spawn PingPong();
var pong = spawn PingPong();

send ping.setPartner(pong);
send pong.setPartner(ping);

Uso del riferimento self

17.7 Ricezione dei messaggi

Dentro un message handler di un actor, puoi usare la funzione receive() per attendere e recuperare il messaggio successivo dalla mailbox dell'actor. Per la maggior parte dei casi d'uso, però, è consigliato usare il messaging call-style con i callback:

actor Echo {
    on echo(msg) {
        var received = receive();
        print($"Echo received: {received}");
    }
}

var echo = spawn Echo();
send echo.echo("hello");  // Pass message as argument

Dettagli della funzione receive

Dichiarazioni di messaggio degli actor (Actor Sugar)

Per actor più strutturati, in stile protocollo, puoi dichiarare i messaggi in modo esplicito nel corpo dell'actor usando la keyword message e gestirli con un loop receive() + match dentro un handler:

actor Counter {
    message Inc(amount);
    message Get() -> int;
    var value = 0;

    on start() {
        var running = true;
        while (running) {
            var msg = receive();
            match msg {
                case Inc(n): value = value + n;
                case Get(): reply(value);
                case ""stop"": running = false;
                default: {};
            }
        }
    }
}

var c = spawn Counter();
send c.start();
send c.Inc(1);
send c.Inc(2);
send c.Get() then (result) {
    print($"Result: {result}");  // Prints: Result: 3
};
send c("stop");

17.8 Message passing tra actor

Gli actor possono inviarsi messaggi a vicenda, abilitando pattern di comunicazione complessi:

actor Sender {
    on start(receiver) {
        print("Sender: Starting...");
        send receiver.hello();
        send receiver.world();
    }
}

actor Receiver {
    var messageCount = 0;
    
    on hello() {
        messageCount = messageCount + 1;
        print($"Receiver: Received 'hello' (total: {messageCount})");
    }
    
    on world() {
        messageCount = messageCount + 1;
        print($"Receiver: Received 'world' (total: {messageCount})");
    }
}

var receiver = spawn Receiver();
var sender = spawn Sender();

send sender.start(receiver);  // Pass receiver reference

17.9 Elaborazione concorrente

Gli actor processano i messaggi in concorrenza. Più actor possono girare contemporaneamente e ogni actor processa i propri messaggi in sequenza:

actor Worker {
    var workerId;
    var taskCount = 0;
    
    function Worker(id) {
        workerId = id;
    }
    
    on processTask(task) {
        taskCount = taskCount + 1;
        print($"Worker {workerId}: Processed task {task} (total: {taskCount})");
    }
}

// Create multiple workers
var workers = [];
for (var i = 0; i < 5; i = i + 1) {
    workers.append(spawn Worker(i + 1));
}

// Small delay to ensure actor loops have started
sleep(100);

// Send tasks to all workers concurrently
for (var i = 0; i < 10; i = i + 1) {
    for (var j = 0; j < workers.length; j = j + 1) {
        send workers[j].processTask(i);
    }
}

// Give workers time to process all tasks
sleep(500);

17.10 Ciclo di vita degli actor

Creazione di un actor

Esecuzione di un actor

Terminazione di un actor

Fermare un actor dall'esterno

Per fermare un actor dall'esterno, chiama il metodo stop() sul riferimento all'actor:

var actor = spawn MyActor();
// ... use the actor ...
actor.stop();  // Stop the actor externally

Fermare un actor tramite messaggio

Gli actor possono anche fermare se stessi gestendo un messaggio di stop. È utile quando l'actor deve fare cleanup o prendere decisioni prima di fermarsi:

actor Worker {
    var isRunning = true;
    var workCount = 0;
    
    on doWork() {
        if (isRunning) {
            workCount = workCount + 1;
            print($"Worker: Completed work #{workCount}");
        }
    }
    
    on stop() {
        print("Worker: Received stop message, shutting down...");
        // Perform any cleanup here
        isRunning = false;
        self.stop();  // Actor stops itself
    }
}

var worker = spawn Worker();
send worker.doWork();
send worker.stop();  // Send stop message

Nota: Dopo aver chiamato stop() (dall'esterno o tramite self.stop()), l'actor finisce di processare il messaggio corrente (se ce n'è uno) e poi esce. Qualsiasi messaggio inviato all'actor dopo che è stato fermato andrà perso.

17.11 Riepilogo delle funzionalità degli actor

Funzionalità principali

Parità tra interprete e modalità transpile

Le funzionalità degli actor sono supportate in modalità interprete, in modalità transpile C# e in modalità backend JavaScript (runtime locale):

Quando compili con --mode transpile, il codice degli actor viene transpile in C# e usa la stessa semantica di runtime dell'interprete. Quando compili con --mode js, il codice degli actor gira sul runtime actor JavaScript locale e preserva la stessa semantica di base per l'ordine della mailbox, i callback, i timeout, receive() e il comportamento di stop.

Limite attuale: gli actor JavaScript sono locali al processo del runtime JavaScript; la comunicazione actor seamless dal browser al server non è abilitata di default in questa fase.

Casi d'uso

Esempio completo

actor Counter {
    var count = 0;
    
    on increment() {
        count = count + 1;
        print($"Count: {count}");
    }
    
    on decrement() {
        count = count - 1;
        print($"Count: {count}");
    }
    
    on reset() {
        count = 0;
        print("Counter reset");
    }
    
    on get() {
        return count;
    }
}

// Spawn and use
var counter = spawn Counter();
send counter.increment();
send counter.increment();
send counter.increment();
send counter.decrement();
send counter.reset();

// Give actor time to process messages (important in transpiled mode)
sleep(500);

Nota: Quando usi gli actor in modalità transpile (eseguibili compilati), è consigliato aggiungere un piccolo delay dopo lo spawn degli actor e alla fine dei programmi, per assicurarti che i messaggi vengano processati prima che il programma termini. Usa sleep(100) dopo lo spawn degli actor e sleep(500) alla fine dei programmi che usano gli actor.

17.12 Usare gli actor con gli agenti

Actor e agenti funzionano bene insieme. Gli agenti sono classi pensate per interazioni AI sincrone; memorizzare gli agenti nello stato di un actor abilita l'elaborazione concorrente e sessioni di agente isolate. È particolarmente utile per:

Esempio:

actor AIAssistant {
    var agent;
    
    function AIAssistant(client) {
        agent = new Agent("Assistant", "helper", "You help users.", client);
    }
    
    on ask(question) {
        var response = agent.think(question);
        print(response.content);
    }
}

var client = new OpenRouterClient();
var assistant = spawn AIAssistant(client);
send assistant.ask("What is MALDA?");

Per informazioni dettagliate su come combinare agenti e actor, inclusi i pattern per l'elaborazione concorrente di agenti e l'orchestrazione multi-agente, vedi Usare gli agenti con gli actor nel capitolo Orchestrazione di agenti.