Journal DSL en golang avec du tagless final

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
9
5
fév.
2018

DSL ou "domain specific langage" est "un langage de programmation dont les spécifications sont conçues pour répondre aux contraintes d’un domaine d'application précis". L'idée est de rapprocher de le plus possible un langage informatique du langage du métier auquel il s'applique.

Dans le cas où l'on veut ajouter un DSL à un autre langage, on peut faire appel au technologique classique de compilation, ou alors il faut essayer d'utiliser le "langage hôte" de la façon la plus intelligente et économe possible.

Au départ, je voulais voir si il était possible de faire un DSL pour SQL, au lieu d'utiliser des "string" dans tous les sens. Cela permet d'avoir plus de vérifications de typage ou de vérification ad-hoc au lieu d'attendre une erreur de la base de données.

J'ai appris l'existence d'une technique ici : https://linuxfr.org/users/kantien/journaux/tagless-final-ou-l-art-de-l-interpretation-modulaire . Le principe est en gros de définir son propre langage avec des fonctions au lieu de le faire avec des données. Fonctions dont la signification est changée selon ce que l'on veut faire (évaluation, pretty printing, vérification de typage,…).

Ocaml utilise ses packages dont des types peuvent être fournis en paramètre. C'est très puissant, et c'est une fonctionnalité quasiment unique dans les langages de programmation.

J'ai tenté de faire la même chose en Go, en partant d'une description de la méthode plus simple faite en programmation objet d'Ocaml. Le but est de pouvoir étendre le DSL et de rajouter des interpréteurs sans toucher au code d'origine.

J'ai trouver le code d'origine ici : http://okmij.org/ftp/tagless-final/course/index.html#lecture

Je voulais voir si on pouvait utiliser cette technique sur un langage ayant un système de typage beaucoup plus simple que Ocaml. Le code suivant n'est pas parfait, je pense que l'on doit pouvoir faire encore plus simple.

    package main

    import (
        "fmt"
        "strconv"
    )
    //(* Tagless Final using dictionary passing *)
    //
    //(* We use objects as extensible records, to model
    //   the implicit dictionary composition in Haskell
    //*)
    //
    //(* Compare with Haskell's ExpSYM *)
    //class type ['repr] expSYM  = object
    //  method lit : int -> 'repr
    //  method neg : 'repr -> 'repr
    //  method add : 'repr -> 'repr -> 'repr
    //end;;

    type expSYM interface {
        lit(int) repr
        neg(repr) repr
        add(repr, repr) repr
    }

    type repr interface{} // pas de type paramètrique en go

    //(* Constructor functions *)
    //let lit n = fun ro -> ro#lit n;;
    //let neg e = fun ro -> ro#neg (e ro);;
    //let add e1 e2 = fun ro -> ro#add (e1 ro) (e2 ro);;

    type exp interface{} // pas d'héritage en go
    type expCons func(exp) repr 
    func lit(n int) expCons { return func(ro exp) repr {return ro.(expSYM).lit(n)} }
    func neg(e expCons) expCons { return func(ro exp) repr {return ro.(expSYM).neg(e(ro))} }
    func add (e1 expCons, e2 expCons) expCons { return func(ro exp) repr {return ro.(expSYM).add(e1(ro),e2(ro))}}

    //(* Unit is for the sake of value restriction *)
    //(* The term is exactly the same as that in Intro2.hs *)
    //let tf1 () = add (lit 8) (neg (add (lit 1) (lit 2)));;
    //

    func tf1 () expCons {
        return add (lit(0),add( lit(8), neg (add( lit(1),lit(2)) )))
    }

    //(* We can write interepreters of expSYM *)
    //(* and evaluate exp in several ways. The code for the interpreters
    //   is quite like the one we have seen already
    //*)
    //class eval = object 
    //  method lit n = (n:int)
    //  method neg e = - e
    //  method add e1 e2 = e1 + e2
    //end;;

    type eval struct{}

    func (eval)lit(n int) repr { return n }
    func (eval)neg(e repr) repr {return - e.(int)}
    func (eval)add (e1 repr,e2 repr) repr {return e1.(int)+e2.(int)}

    //class view = object
    //  method lit n = string_of_int n
    //  method neg e = "(-" ^ e ^ ")"
    //  method add e1 e2 = "(" ^ e1 ^ " + " ^ e2 ^ ")"
    //end;;
    type view struct{}

    func (view)lit(n int) repr { return strconv.Itoa(n) }
    func (view)neg(e repr) repr {return "(-" + e.(string) + ")"}
    func (view)add (e1 repr,e2 repr) repr {return "("+e1.(string)+"+"+e2.(string)+")"}

    //(* We can extend our expression adding a new expression form *)
    //class type ['repr] mulSYM = object
    //  method mul : 'repr -> 'repr -> 'repr
    //end;;
    //
    //let mul e1 e2 = fun ro -> ro#mul (e1 ro) (e2 ro);;

    type mulSYM interface {
    //  expSYM
        mul(repr, repr) repr
    }
    // type mulCons func(mulSYM) repr //can't be inherited
    func mul (e1 expCons, e2 expCons) expCons { return func(ro exp) repr {return ro.(mulSYM).mul(e1(ro),e2(ro))}}

    //(* Extended sample expressions *)
    //(* Again, the code is the same as before, modulo the occasional () *)
    //(* Value restriction is indeed annoying ... *)
    //let tfm1 () = add (lit 7) (neg (mul (lit 1) (lit 2)));;
    //
    //let tfm2 () = mul (lit 7) (tf1 ());;

    func tfm1 () expCons {
        return add ( lit (7), neg (mul (lit (1), lit (2))));;
    }
    func tfm2 () expCons {
        return mul(lit (7), tf1 ())
    }

    //class evalM = object 
    //  inherit eval
    //  method mul e1 e2 = e1 * e2
    //end;;
    //
    //let evalM = new evalM;;

    type evalM struct{eval}

    func(evalM)mul(e1 repr,e2 repr) repr {return e1.(int)*e2.(int)}

    //class viewM = object
    //  inherit view
    //  method mul e1 e2 = "(" ^ e1 ^ " * " ^ e2 ^ ")"
    //end;;
    type viewM struct{view}
    func (viewM)mul (e1 repr,e2 repr) repr {return "("+e1.(string)+"*"+e2.(string)+")"}


    func main() {
        //let eval = new eval;;
        //
        //(* We didn't apply eval, we pass eval as an argument *)
        //let 5  = tf1 () eval;;
    var eval eval
    res := tf1()(eval)
    fmt.Printf("%#v\n",res);
    //let view = new view;;
    //
    //let "(8 + (-(1 + 2)))" = tf1 () view;;
    var view view
    res2 :=tf1()(view)
    fmt.Printf("%#v\n",res2)
    //let evalM = new evalM;;
    //(* can use the extended evaluator to evaluate old expressions *)
    //let 5  = tf1 () evalM;;

    var evalM evalM
    res = tf1()(evalM)
    fmt.Printf("%#v\n",res);

    //let viewM = new viewM;;
    var viewM viewM
    res = tf1()(viewM)
    fmt.Printf("%#v\n",res)

    res = tfm1()(viewM)
    fmt.Printf("%#v\n",res)
    res = tfm1()(evalM)
    fmt.Printf("%#v\n",res)
    res = tfm2()(viewM)
    fmt.Printf("%#v\n",res)
    res = tfm2()(evalM)
    fmt.Printf("%#v\n",res)
}

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.