Interprète de modèle

Ce qui est inclus

Le modèle Interpreter fait référence aux modèles de conception comportementale. Ce modèle vous permet d’implémenter votre propre langage de programmation en travaillant avec un arbre AST dont les sommets sont des expressions terminales et non terminales qui implémentent la méthode Interpret, qui fournit les fonctionnalités du langage.

  • Expression de terminal : par exemple, constante de chaîne – “Bonjour tout le monde”
  • Expression non terminale : par exemple Print(“Hello World”), contient Print et un argument de l’expression terminale “Hello World”

Quelle est la différence ? La différence est que l’interprétation se termine sur les expressions terminales, mais pour les expressions non terminales, elle se poursuit en profondeur sur tous les sommets/arguments entrants. Si l’arborescence AST était constituée uniquement d’expressions non terminales, l’application ne se terminerait jamais, car une certaine finitude de tout processus est requise, cette finitude est ce que sont les expressions terminales, elles contiennent généralement des données, par exemple des chaînes.

Un exemple d’arborescence AST est ci-dessous :


Dcoetzee, CC0, via Wikimedia Commons

Comme vous pouvez le voir, les expressions terminales sont constantes et variables, les expressions non terminales sont le reste.

Ce qui n’est pas inclus

L’implémentation de l’Interpreter n’inclut pas l’analyse de la chaîne de langue entrée dans l’arborescence AST. Il suffit d’implémenter des classes d’expressions terminales et non terminales, des méthodes Interpret avec l’argument Context en entrée, de créer un arbre d’expressions AST et d’exécuter la méthode Interpret au niveau de l’expression racine. Un contexte peut être utilisé pour stocker l’état de l’application au moment de l’exécution.

Mise en œuvre

Le modèle implique :

  • Client – ​​​​renvoie l’arborescence AST et exécute Interpret(context) pour le nœud racine (Client)
  • Contexte : contient l’état de l’application, transmis aux expressions lorsqu’elles sont interprétées (Contexte)
  • Expression abstraite : une classe abstraite contenant la méthode Interpret(context) (Expression)
  • L’expression terminale est une expression finale, descendante d’une expression abstraite (TerminalExpression)
  • Une expression non terminale n’est pas une expression finie ; elle contient des pointeurs vers des sommets profonds dans l’arborescence AST ; les sommets subordonnés affectent généralement le résultat de l’interprétation de l’expression non terminale (NonTerminalExpression)

Exemple client en C#

        static void Main(string[] args)
        {
            var context = new Context();
            var initialProgram = new PerformExpression(
                new IExpression[] {
                    new SetExpression("alpha", "1"),
                    new GetExpression("alpha"),
                    new PrintExpression(
                        new IExpression[] {
                            new ConstantExpression("Hello Interpreter Pattern")
                        }
                    )
                }
            );
            System.Console.WriteLine(initialProgram.interpret(context));
        }
}

Exemple d’expression abstraite en C#

{
    String interpret(Context context);
}

Exemple d’expression de terminal en C# (constante de chaîne)

{
    private String constant;

    public ConstantExpression(String constant) {
        this.constant = constant;
    }

    override public String interpret(Context context) {
        return constant;
    }
}

Exemple d’expression non terminale en C# (démarrage et concaténation des résultats des sommets subordonnés, en utilisant le délimiteur « ; »

{
    public PerformExpression(IExpression[] leafs) : base(leafs) {
        this.leafs = leafs;
    }
    
    override public String interpret(Context context) {
        var output = "";
        foreach (var leaf in leafs) {
            output += leaf.interpret(context) + ";";
        }
        return output;
    }
}

Pouvez-vous le faire fonctionnellement ?

Comme on le sait, tous les langages Turing-complets sont équivalents. Est-il possible de transférer le modèle orienté objet vers le langage de programmation fonctionnel ?

Pour une expérience, prenons un langage FP pour le Web appelé Elm. Il n’y a pas de classes dans Elm, mais il y a des enregistrements et des types, donc les enregistrements et types suivants sont impliqués dans l’implémentation :

  • Expression : liste de toutes les expressions linguistiques possibles (Expression)
  • Expression subordonnée : expression subordonnée à l’expression non terminale (ExpressionLeaf)
  • Contexte : un enregistrement stockant l’état de l’application (Contexte)
  • Fonctions qui implémentent les méthodes Interpret(context) : toutes les fonctions nécessaires qui implémentent les fonctionnalités des expressions terminales et non terminales
  • Enregistrements auxiliaires de l’état de l’interprète – nécessaires au bon fonctionnement de l’interprète, ils stockent l’état de l’interprète, le contexte

Un exemple de fonction qui implémente l’interprétation pour l’ensemble des expressions possibles dans Elm :

  case input.expression of
    Constant text ->
      { 
        output = text, 
        context = input.context 
      }
    Perform leafs ->
      let inputs = List.map (\leaf -> { expressionLeaf = leaf, context = input.context } ) leafs in
        let startLeaf = { expressionLeaf = (Node (Constant "")), context = { variables = Dict.empty } } in
          let outputExpressionInput = List.foldl mergeContextsAndRunLeafs startLeaf inputs in
            {
              output = (runExpressionLeaf outputExpressionInput).output,
              context = input.context
            }
    Print printExpression ->
      run 
      { 
        expression = printExpression, 
        context = input.context 
      }
    Set key value ->
      let variables = Dict.insert key value input.context.variables in
      {
        output = "OK",
        context = { variables = variables }
      }
    Get key ->
      {
        output = Maybe.withDefault ("No value for key: " ++ key) (Dict.get key input.context.variables),
        context = input.context
      }

Et l’analyse syntaxique ?

L’analyse du code source dans une arborescence AST n’est pas incluse dans le modèle Interpreter ; il existe plusieurs approches pour analyser le code source, mais nous y reviendrons une autre fois.
Dans l’implémentation de l’Interpreter for Elm, j’ai écrit un analyseur simple dans l’arborescence AST, composé de deux fonctions : analyser un sommet et analyser les sommets subordonnés.

parseLeafs state =
    let tokensQueue = state.tokensQueue in
        let popped = pop state.tokensQueue in
            let tokensQueueTail = tail state.tokensQueue in
                if popped == "Nothing" then
                    state
                else if popped == "Perform(" then
                    {
                        tokensQueue = tokensQueue,
                        result = (state.result ++ [Node (parse tokensQueue)])
                    }
                else if popped == ")" then
                    parseLeafs {
                        tokensQueue = tokensQueueTail,
                        result = state.result
                    }
                else if popped == "Set" then
                    let key = pop tokensQueueTail in
                        let value = pop (tail tokensQueueTail) in
                            parseLeafs {
                                tokensQueue = tail (tail tokensQueueTail),
                                result = (state.result ++ [Node (Set key value)])
                            }
                else if popped == "Get" then
                    let key = pop tokensQueueTail in
                        parseLeafs {
                            tokensQueue = tail tokensQueueTail,
                            result = (state.result ++ [Node (Get key)])
                        }
                else 
                    parseLeafs {
                        tokensQueue = tokensQueueTail,
                        result = (state.result ++ [Node (Constant popped)])
                    }

parse tokensQueue =
    let popped = pop tokensQueue in
        let tokensQueueTail = tail tokensQueue in
            if popped == "Perform(" then
                Perform (
                    parseLeafs {
                        tokensQueue = tokensQueueTail, 
                        result = []
                    }
                ).result
            else if popped == "Set" then
                let key = pop tokensQueueTail in
                    let value = pop (tail tokensQueueTail) in
                        Set key value
            else if popped == "Print" then
                Print (parse tokensQueueTail)
            else
                Constant popped

Liens

https://gitlab.com/demensdeum /patterns/-/tree/master/interpreter/elm
https://gitlab.com/demensdeum/patterns/-/tree/master/interpreter/csharp

Sources

https://en.wikipedia.org/wiki/Interpreter_pattern
https://elm-lang.org/
https://docs.microsoft.com/en-us/dotnet/csharp/

Leave a Comment

Your email address will not be published. Required fields are marked *