Tic-tac-Jolie

IV parte: Il microservizio “giocatore”di e

Last time on Tic-tac-Jolie

Nella parte precedente abbiamo

  • commentato il codice dell’operazione listOpenGames;
  • commentato il codice dell’operazione startGames;
  • introdotto il concetto di operazione interna e commentato il codice dell’operazione initiateGame che lo implementa.

Il codice d’implementazione del microservizio “giocatore”

Vediamo, con commenti, il codice di implementazione del microservizio “giocatore”.

 

include “TrisGameInterface.iol”
include “UserInterface.iol”
include “console.iol”

console ci permetterà di usare in() come operazione di input da tastiera, e println@Console come operazione per stampare sullo schermo.

 

outputPort TrisGame {
    Location: “socket://localhost:9000”
    Protocol: sodep
    Interfaces: TrisGameInterface
}
inputPort User {
    Location: UserLocation
    Protocol: sodep
    Interfaces: UserInterface
}

TrisGame è la porta di uscita che punta al gestore della scacchiera. User è la porta d’ingresso per gli aggiornamenti ricevuti dal gestore della scacchiera.

 

define drawMap {
    for( i = 0, i < 9, i++ ) {
        if ( i%3 == 0 ) { println@Console(“-----------”)() };
        if ( sync_req.places[ i ] == 0 ) {
            place_content = “ “
        };
        if ( sync_req.places[ i ] == 1 ) {
            place_content = “O”
        };
        if ( sync_req.places[ i ] == -1 ) {
            place_content = “X”
        };
        print@Console( “ “ + place_content + “ “ )();
        if ( (i%3 == 0) || (i%3 == 1) ) { print@Console(“|”)() };
        if ( i%3 == 2 ) { println@Console()() }
    }
}

Questo codice stampa la mappa a video.

 

init {
    registerForInput@Console()()
}

Punto d’inizializzazione del servizio: ci si connette al flusso di input della Console.

 

main {
    listOpenGames@TrisGame()( list_games );
    /* play with the first game if it exists, otherwise create a new game */
    if ( #list_games.game_token > 0 ) {
        strt_req.game = list_games.game_token[ 0 ]
    };
    strt_req.user_location = UserLocation;
    startGame@TrisGame( strt_req )( game );
    end_game = false;
    while( !end_game ) {
        syncPlaces( sync_req );

Se vi è una scacchiera disponibile, gioca su quella scacchiera; altrimenti, con la chiamata a startGame, crea una nuova scacchiera e attende una chiamata a syncPlaces.

 

            drawMap; 
            println@Console( sync_req.message )();
            if ( sync_req.status_game != “end” ) {
                if ( sync_req.status_game == “play” ) {
                    made_move = false;
                    while(!made_move ) {

Stampa la mappa. Se non siamo alla fine del gioco e se è il nostro turno (status_game == play), iniziamo il loop di gestione delle mosse del giocatore.

 

               scope( move ) {
                     install( MoveNotAllowed
                              => println@Console(
move.MoveNotAllowed )() );
                          print@Console(“Insert the place you
want to set (from 0 to 8,
                                 counting from left top corner                                   to thebottom right one):”)();

Definisce lo scope all’interno del quale gestire l’eventuale eccezione che giunga dopo aver effettuato la mossa. L’eccezione che ci aspettiamo è MoveNotAllowed. Nel caso sia ricevuta tale eccezione, verrà visualizzato a schermo il messaggio di errore.

Usiamo il costrutto linguistico di input choice in cui definiamo due operazioni in ingresso (messaggi) mutualmente esclusivi. In questo modo gestiamo l’asincronismo tra l’input del giocatore e i messaggi di sincronizzazione che giungono dalla scacchiera.

In un determinato momento, infatti, vi è la possibilità che il giocatore voglia immettere la sua mossa o che arrivi un messaggio di sincronizzazione. Mettendo le due operazioni come scelta mutualmente esclusiva, imponiamo una sequenzializzazione della gestione eventi.

 

                    [ in( answer ) ] {
                       println@Console()();
                       with( mv ) {
                         .game_token = game.game_token;
                         .participant_token = game.role_token;
                         .place = int( answer )
                       };
                       move@TrisGame( mv )();
                       made_move = true
                    }

Input della mossa da parte del giocatore. Chiamo l’operazione move sulla porta Trisgame per comunicare al gestiore di scacchiere la mossa.

 

                      [ syncPlaces( sync_req ) ] {
                        if (sync_req.status_game == “end”) {
                           end_game = true;
                           println@Console()();
                        println@Console(sync_req.message)();
                           made_move = true
                        }
                     }
                  }
               }
           }
       } else {
           end_game = true
       }
    }

}

Se riceviamo un messaggio di aggiornamento stato tastiera (gioco), verifichiamo se il gioco è finito, nel qual caso si setta end_game = true che fa terminare il servizio.

 

Appronfondimenti sintattici e semantici

Vediamo qui di seguito alcuni approfondimenti che riguardano sintassi e semantica del nostro linguaggio.

Correlation sets (sessioni)

Speriamo che qualche lettore sia chiesto: “Ma se stiamo giocando in tanti e contemporaneamente, come si riesce a far arrivare la mossa giusta alla scacchiera interessata e non a un’altra?”.

Entra qui in gioco il concetto di sessione. Le sessioni sono conversazioni aperte, con stati, che interagiscono con altre entità in vista di un obiettivo.

Normalmente un’istanza di un behaviour (processo) esegue il codice, termina e, alla successiva chiamata, non ha memoria delle precedenti invocazioni.

Ma nell’operatività di tutti i giorni noi abbiamo a che fare con sessioni, cioè con una sequenza di operazioni che dipendono dalla precedente (cioè dalla storia passata). Durante tali sessioni i messaggi debbono essere indirizzati alla sessione corretta: un messaggio di annullamento di pagamento non deve essere inviato a una sessione ove ancora siamo in fase di scelta articoli o, peggio, di pagamento di un altra persona.

Per gestire la corretta individuazione del behaviour in esecuzione, Jolie introduce il concetto di correlation sets (“insiemi di correlazione”, per la sintassi, si faccia riferimento al punto A.11 dell’appendice che verrà pubblicata alla fine della serie). Tale concetto generalizza il session identifier utilizzato normalmente in altri paradigmi per risolvere tale identificazione. I correlation set definiscono un insieme di variabili con cui identificare la sessione corretta con cui il messaggio deve interagire.

Gli insiemi di correlazione riguardano sia il deployment, indicando all’interprete come associare i messaggi in ingresso ai corrispondenti processi (istanze di behaviour), sia il behaviour, specificando come si possano assegnare dei valori alle variabili di correlazione.

Gli insiemi di correlazione sono liste di variabili di correlazione associate ai loro alias. Nel nostro gioco, la dichiarazione del correlation set è la seguente

cset {
    token: MoveRequest.game_token
}

Noi abbiamo una sola variabile di correlazione (token) che ha come suo alias il valore della variabile game_token all’interno del tipo MoveRequest che è un tipo associato all’operazione d’input move().

Quindi nel momento di arrivo del messaggio move(MoveRequest)(void), il sistema interroga i suoi alias trovando che a MoveRequest è associato un solo alias per quanto riguarda il campo game_token.

Esaminata quindi la variabile di correlazione token dei differenti processi e quella il cui valore sarà uguale al valore in input nell’operazione move, determinerà il processo (scacchiera) che riceverà la mossa.

Gli alias (VarPath nel diagramma sintattico) fanno riferimento a variabili all’interno di un tipo associato a un messaggio d’ingresso. Questo rende possibile una verifica statica di essi: corrispondenza a una variabile dichiarata nel tipo in esame, variabile non opzionale.

Vediamo che, per ogni operazione d’input dobbiamo avere un insieme di correlazione che correla ogni variabile del messaggio d’input che sia necessaria. L’assegnazione avviene dentro initiateGame

csets.token = request.game_token;

Nella variabile dell’insieme di correlazione mettiamo il valore associato alla scacchiera che generammo a suo tempo con new, e che associammo alla risposta di startGame.

Tale valore venne poi restituito — assieme agli altri presenti — al secondo giocatore in sede di listOpenGames.

Questo stesso parametro (token) lo ripassiamo a initiateGame affinché diventi il valore della sola variabile di correlazione presente. Appare qui l’importanza dell’istruzione new che genera un nuovo valore non generato precedentemente, da poter usare come valore univoco in un insieme di correlazione.

Notiamo come le operazioni listOpenGames, startGame non abbiano bisogno di variabili di correlazione: per esse il processo creato non ha bisogno di ricordarsi la storia passata. Inizia il suo ciclo di vita e poi lo finisce.

Rimandiamo alla documentazione ufficiale di Jolie per approfondire altre caratteristiche degli insiemi di correlazione.

I dati e le variabili in Jolie

Le variabili in Jolie vengono definite a tempo di esecuzione (dynamic typing), le variabili cioè non debbono essere dichiarate precedentemente e il loro tipo è determinato a run time.

Le variabili sono concettualmente la radice di una struttura dati ad albero come in XML e in JSON, ed è possibile navigare in esse mediante l’operatore ‘.’. Tali percorsi (path) sono valutati a tempo di esecuzione e potranno contenere espressioni letterali e anche delle variabili. Vediamo una serie di esempi.

 

path letterale

x.y.z

Accedo alla radice x proseguo per il figlio y e dentro di esso accedo a z

 

x.y.z = 10

Crea la variabile x che contiene tale struttura.

 

path con variabile

x.(“y”).z

Accedo alla radice x, valuto il contenuto della variabile y, il valore ottenuto (stringa) è aggiunto al path e con il path parziale così costruito cerco il figlio z

Se y = xxxx.(“y”).z viene risolto nel path x.xxx.z

 

Le variabili hanno una vita confinata all’operazione in cui sono definite. È possibile dichiarare variabili globali aggiungendo global come prefisso al path. Ecco un esempio:

global.games.( token ).circle_participant

Accedo a games (variabile globale), valuto la variabile token , il risultato deve essere un identificatore con il quale cerco l’omonimo figlio di games. Una volta trovatolo, accedo a circle_participant (figlio di quest’ultimo). Quindi. se token = xyz1

global.games.xyz1.cicle_participant

Se global.games.(token) non corrisponde a nessun path, allora la variabile risulta non definita.

Oltre a definire una variabile dinamicamente come indicato sopra, è possibile associare una variabile a un tipo di dati. Ciò avviene quando nel codice indichiamo una variabile in una operazione one way oppure request-response. Ad esempio, in

listOpenGames@TrisGame()( list_games )

la variabile list_games è associata al tipo di dati ListOpenGamesResponse.

Nel definire i tipi di dati, ad ogni nodo è possibile assegnare una cardinalità, ossia il numero minimo e massimo di occorrenze  (come si vedrà nell’appendice, al punto A.5, cardinalità tipi). Ciò è simile a definire un’array in altri linguaggi.

  • .field [2,5] = campo che appare almeno 2 volte e al massimo 5;
  • .field ? = .field[0,1] = campo opzionale, può apparire al massimo una volta;
  • .field * = campo che può apparire oppure no, o anche apparire un numero indefinito di volte.

L’operatore di accesso ai campi che hanno un’occorrenza multipla, è [ ]. Ad esempio

response.game_token[ count ] = g

significa che accedo al nodo game_token con il cui numero ordinale è count, e ad esso assegno il contenuto della variabile g.

 

Conclusioni

In questa IV parte della serie abbiamo commentato il servizio che implementa il giocatore; approfondito come Jolie riesce, tramite lo strumento dei correlation sets, a correlare i messaggi ai rispettivi processi (istanze del behaviour) a cui sono logicamente destinati, in quanto facenti parte della stessa sessione; approfondito la strutturazione ad albero dei tipi di dati di Jolie e della valutazione dei loro percorsi di accesso in maniera dinamica.

Nel prossimo articolo vedremo le porte e l’importazione di file esterni.

 

 

Riferimenti

[1] Il linguaggio Jolie

http://www.jolie-lang.org/

 

[2] Un’introduzione ai microservizi

https://en.wikipedia.org/wiki/Microservices#Introduction

 

[3] Il codice nel repository

https://github.com/jolie/examples/blob/master/02_basics/5_sessions/tris/tris.ol

 

 

Condividi

Pubblicato nel numero
268 gennaio 2021
Ha cominciato con l'informatica da autodidatta, nel lontano 1982 — anni ruggenti — con il mitico Lemon II (clone di Apple II) e registratore a cassette. Ha poi conseguito una laurea vecchio ordinamento in ingegneria elettronica indirizzo informatica presso l’Università degli Studi di Bologna. Negli anni, ha spaziato in diversi…
Claudio Guidi è un ricercatore ed un imprenditore nell‘area dei microservices. Co-Leader del progetto Jolie (http://www.jolie-lang,org), ha conseguito il suo Ph.D. in computer science presso l‘Università di Bologna con una tesi sulla formalizzazione dei linguaggi per il Service Oriented Computing. Insieme a Fabrizio Montesi, l‘altro creatore di Jolie, ha fondato…
Articoli nella stessa serie
Ti potrebbe interessare anche