Utilisation de la grammaire M d’OSLO pour créer des arbres d’expression lambda

Contexte

Dans le cadre d’une étude sur la prévalence, un besoin a été émis de générer de la requête ad-hoc sur les ensembles de données. La demande émerge souvent des discussions sur la prévalence, et la question m’a été posée à chacune des trois conférences AgileTour dernièrement.

Ma réponse habituelle est de montrer l’utilité de Linq To Objects pour le requêtage des objets prévalents, et un précédent article a déjà montré le mécanisme et ses excellentes performances. Mais il reste deux points à traiter…

Le premier est le SQL historique. Quand on a 30 000 requêtes pour du reporting, il serait intéressant de pouvoir transformer automatiquement ceci en requêtes Linq sur la base prévalente. Je suis en train de créer un parseur de SQL en lien avec du Linq To Objects et j’espère pouvoir vous en parler dans quelques mois. Evidemment, le but ultime sera d’avoir un provider ADO.NET branchable sur un serveur prévalent, mais il y a beaucoup de travail. Au passage, si quelqu’un est intéressé par un projet OpenSource autour de ceci, merci de me contacter.

Le second est un peu plus simple, heureusement : lorsque des requêtes étaient créées à la volée par des mécanismes de requêteur comme il en existe dans de nombreuses applications de gestion, il est intéressant de pouvoir générer automatiquement le Linq To Objects correspondant. Je pense que le projet VisualLinq de Mitsuru Furuta peut donner de bonnes idées, mais dans un premier temps, voici une proposition de résolution basée sur la grammaire M d’Oslo, ainsi qu’une construction dynamique des arbres d’expression lambda.

OSLO et la grammaire M

OSLO est une technologie Microsoft orientée vers la programmation des modèles. On pourrait longuement parler sur son intérêt pour le Domain Driven Development et les DSL, mais on va se concentrer pour cet article sur la grammaire M que ce framework propose, en partant d’un exemple simplissime.

Pour cela, je renvoie sur l’excellent blog http://www.techheadbrothers.com/Articles.aspx/evaluation-expressions-mathematiques-mgrammar-page-1 à partir duquel j’ai repris l’exemple ci-dessous. Mais la source la plus complète pour des informations sur M est le blog de référence, à savoir http://blogs.msdn.com/b/modelcitizen/

La grammaire M que nous allons étudier est la suivante :

module MCalc
{
    language MCals
    {
        // Options principales
        syntax Main = Expression;
        interleave Inutile = Espace;
        token Espace = ‘\r’ | ‘\n’ | ‘ ‘;

        // Définition des nombres
        token Integer = Digits+;
        token Digits = ‘0’ .. ‘9’;
        token Number = Integer? ‘.’? Integer;
           
        // Gestion des enchaînements
        syntax Expression
            = x:Number => x
            | g:Expression left(1) ‘+’ d:Expression => Plus {g,d}
            | g:Expression left(1) ‘-‘ d:Expression => Minus {g,d}
            | g:Expression left(2) ‘*’ d:Expression => Times {g,d}
            | g:Expression left(2) ‘/’ d:Expression => Divide {g,d}
            | g:Expression right(3) ‘^’ d:Expression => Power {g,d}
            | left(4) ‘(‘ g:Expression ‘)’ => g;
    }
}

Le principe de la grammaire M est de définir d’autres grammaires. En ce sens, M est un méta-langage. Ici, on est simplement en train de définir une expression comme étant un ensemble complexe d’expressions arithmétiques, avec en fin de récursion un nombre composé de digits, à savoir des symboles compris entre 0 et 9. Il y a bien sûr d’autres détails intéressants qui font toute la force de M, à savoir la gestion automatique des espaces (au sens large, c’est-à-dire tout caractère n’entrant pas dans la définition de l’expression), la prise en charge de la cardinalité, l’écriture sous forme fonctionnelle, etc.

Comment utiliser cette grammaire

Maintenant, il faut bien avouer que c’est un peu aride, comme ça. Donc, première chose à faire : télécharger OSLO. Ce lien donnera le download, ainsi que des détails supplémentaires sur ce qu’est OSLO.

Lors de l’installation d’OSLO, ne vous embêtez pas avec les autres options que le SDK : il contient le langage M et cela nous suffira.

image

Une fois qu’on a installé le produit, on a dans le menu démarrer accès à un éditeur de grammaire M.

image

Attention : pour les utilisateurs d’une CTP plus ancienne, veillez à le lancer en mode exemple. A mon avis, c’est un bug, mais le lancement normal ne permet pas d’ouvrir des fichiers pré-existants correctement.

Par contre, la difficulté pour ouvrir une session de test de grammaire est bien restée de l’ancienne CTP à la nouvelle. Si vous copiez-collez l’exemple, c’est bon, par contre, si vous souhaitez ensuite repartir du fichier .mg que vous aurez sauvegardé, le raccourci clavier est CTRL-SHIFT-T. Oui, c’est compliqué, mais c’est le principe des CTP…

Bref, une fois chargée la grammaire, vous vous retrouvez avec quelque chose comme ceci :

image

On commence mal, avec une erreur qu’il n’y avait pas sur la CTP précédente. Pour régler ça de manière rapide et brutale, on vire simplement le signe ? qui pose problème. OSLO considère visiblement qu’il y a un problème de cardinalité. Du coup, ça passe mieux, et on peut taper un premier exemple, montrant que la décomposition fonctionne correctement :

image

Rien que ce qui apparaît sur la droite est déjà très intéressant. Je peux penser à de nombreux cas d’analyse de SQL complexe où j’aurais bien aimé avoir ce système de décomposition arborescente pour retrouver la trame de la requête…

Générer l’API correspondante

Mais le plus intéressant est que vous allez pouvoir générer du code C# qui va effectuer la même décomposition sous la forme d’API, et vous renvoyer des classes générées pour vous et qui représentent les différents concepts que vous aurez créés dans la grammaire. En l’occurence, vous aurez accès à des instances de Expression, Plus, Minus, etc. tels que la grammaire les définit.

Pour cela, il suffit de lancer en ligne de commande mg.exe, que vous trouverez dans C:\Program Files\Microsoft Oslo\1.0\bin ou quelque chose approchant en fonction de vos paramètres système. Comme seul paramètre, mettez /t:source [votre fichier .mg]. Sur la nouvelle CTP, l’utilitaire a apparemment été renommé en m.exe, et on peut passer directement le fichier de grammaire en paramètre de ligne de commande, sans code d’argument. Par contre, j’ai eu un peu de mal à trouver les options pour générer du code : il semble qu’il y ait eu beaucoup de changement dans le mode d’utilisation de l’utilitaire. Bref, pour continuer ce post, je me baserai sur la CTP d’Octobre 2008, et je renvoie le lecteur à la doc. Si quelqu’un va au bout de l’exercice, je lui serais très reconnaissant de mettre un commentaire en expliquant la nouvelle grammaire d’appel.

Le résultat est un fichier source complexe, dans lequel vous allez trouver ce type d’entrées :

[System.Dataflow.TokenAttribute(((System.Dataflow.TokenModifiers)(0)), "__AnonymousToken2", "")]
internal class @__AnonymousToken2 {
    
    public static System.Dataflow.ParseTokenReference @__ReturnType;
    
    public class @__Production0 {
        
        [System.Dataflow.TermAttribute(0)]
        internal static @__lit7 Term0;
    }
}

[System.Dataflow.LiteralAttribute("*")]
internal class @__lit7 {
}

Ces deux classes internes servent par exemple pour la décomposition des expressions autour du symbole “*”. Si on se place un peu plus haut, on voit la définition de la classe principale, à savoir la classe spécialisant le parseur :

public class MCals : System.Dataflow.ParserBase {
        
    static System.Dataflow.ParserFactory @__parserFactory = System.Dataflow.ParserBase.LoadParser(typeof(MCalc.MCals), null);
        
    static System.Dataflow.ILexer @__lexer = System.Dataflow.ParserBase.LoadLexer(typeof(MCalc.MCals));

En appelant la fonction publique Parse de cette classe (détails sur le blog TechHeadBrothers dont j’ai donné le lien plus haut), vous obtiendrez un objet spécialisé que vous pourrez parcourir à l’aide d’un GraphBuilder. C’est là qu’on va pouvoir passer aux choses sérieuses…

Génération d’expression lambda

Pour revenir à notre problème initial, il s’agissait de pouvoir créer dynamiquement un prédicat de restriction sur une expression Linq To Objects.

Typiquement, en dur on a quelque chose comme suit :

List<Personne> Liste = new List<Personne>();
Liste.Add(new Personne() { Nom = "Gouigoux", Prenom = "JP" });
Liste.Add(new Personne() { Nom = "Lagaffe", Prenom = "Gaston" });

var Resultat = Liste.Where(p => p.Nom == "Gouigoux");

Mais on souhaite pouvoir créer dynamiquement l’expression lambda passée à Where. Pour cela, la solution est de créer un expression lambda en utilisant la classe System.Linq.Expressions.Expression. Dans notre exemple, cela donnerait :

var param = Expression.Parameter(typeof(Personne), "p");
var lambda = Expression.Lambda<Func<Personne, bool>>(
    Expression.Equal(
        Expression.Property(param, "Nom"),
        Expression.Constant("Gouigoux")),
    param);

Et c’est là que ça devient intéressant : ces enchainements d’appels à des méthodes peuvent s’étendre à l’infini. Supposons par exemple que l’on souhaite désormais générer l’expression lambda associée à la condition p.Nom.StartsWith(“Gouigoux”). On aura le code suivant :

lambda = Expression.Lambda<Func<Personne, bool>>(
        Expression.Call(
            Expression.Property(param, "Nom"), 
            typeof(string).GetMethod("StartsWith", new [] { typeof(string) }),
            Expression.Constant("Gouigoux")),
    param);

Utiliser l’API issue de M pour générer des expressions lambda à destination de Linq

Bref, on peut à peu près tout faire, et surtout, on peut facilement adapter ceci à un parcours récursif d’une grammaire M avec des patterns de type visiteur. Lorsqu’on tombe sur un opérateur NON, par exemple, on peut simplement appeler une fonction renvoyant la négation d’une expression comme ceci :

static Expression RenvoieNegation(Expression Exp)
{
    return Expression.Not(Exp);
}

Si on crée autant de ce type de fonctions que de concepts dans la grammaire M, il suffit ensuite de créer l’expression en partant comme point de départ de l’objet issu du parsing.

Enfin, c’est le plus important et j’avais failli l’oublier, il faut bien compiler l’expression lambda en un prédicat adapté pour pouvoir l’appliquer sur un Where de Linq, par exemple :

List<Personne> Liste = new List<Personne>();
Liste.Add(new Personne() { Nom = "Gouigoux", Prenom = "JP" });
Liste.Add(new Personne() { Nom = "Lagaffe", Prenom = "Gaston" });

lambda = Expression.Lambda<Func<Personne, bool>>(
        Expression.Call(
            Expression.Property(param, "Nom"), 
            typeof(string).GetMethod("StartsWith", new [] { typeof(string) }),
            Expression.Constant("Gouigoux")),
    param);
Predicat = lambda.Compile();
ResultatLambda = Liste.Where<Personne>(Predicat);
foreach (Personne p in ResultatLambda)
    Console.WriteLine(p.Nom);

Note : on n’est pas obligé de préciser <Personne> sur le Where : .NET serait capable de l’inférer, mais je trouve ça un peu plus clair de le préciser lorsqu’on fait de la contrainte dynamique.

Conclusion

En mélangeant l’usage de grammaire M et de la composition dynamique d’expressions lambda, il est tout à fait possible de générer des requêtes Linq ad-hoc en fonction d’une grammaire entrante. Cette grammaire peut très bien être du SQL, ce qui répond à de nombreuses problématiques en prévalence, mais elle peut également correspondre à une grammaire propriétaire.

Supposons qu’une grammaire “COMMENCE_PAR(NOM, CONSTANTE(GOUIGOUX))” existe sous forme arborescente ou même textuelle dans une base de données, on pourra définir une grammaire M réalisant le parsing de cette expression, puis mettre en place une récursion de génération d’expression lambda remplaçant COMMENCE_PAR par un appel lambda à StartsWith et créant une constante avec Expression.Constant. Enfin, la compilation et l’intégration dans une requête Linq par le Where générique permettront d’exécuter cette contrainte sur une liste existante.

[Edit]

A noter qu’il y a aussi la possibilité, pour les cas très simples, d’utiliser XPathObjectNavigator, qui vous permettra de parcourir les objets d’un système prévalent en utilisant du XPath. Et du coup, c’est facile de faire des requêtes dynamiques par simple concaténation de texte.

About JP Gouigoux

Jean-Philippe Gouigoux est Architecte Logiciel, MVP Connected Systems Developer. Il intervient régulièrement à l'Université de Bretagne Sud ainsi qu'à l'Agile Tour. Plus de détails sur la page "Curriculum Vitae" de ce blog.
This entry was posted in .NET, Veille and tagged , . Bookmark the permalink.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Captcha Captcha Reload