J'ai joué un peu avec le code, surtout l'encodage de Bohem-Berarducci, pour comprendre un peu ce qu'il faisait et comment cela marchait. Je vais l'illustrer sur un langage simple : un opérateur binaire sur les entiers (un semi-groupe pour les adeptes de l'algèbre ;-).
On commence de manière classique avec un type exp pour l'AST du langage :
typeexp=Litofint|Opofexp*exp
on se donne des smart constructors pour notre langage et une fonction fold générique sur son AST :
à partir de là, on peut définir tout un tas d'interprétations différentes de l'AST et on colle le tout dans un module Ast :
moduleAst=struct(* l'AST du langage à un opérateur binaire sur les entiers *)typeexp=|Litofint|Opofexp*exp(* smart constructors *)letlitn=Litnletopee'=Op(e,e')(* fold générique sur l'AST *)letrecfoldfg=function|Litn->fn|Op(e,e')->g(foldfge)(foldfge')(* interprétation en tant qu'addition *)letplus=fold(funi->i)(+)(* interprétation en tant que soustraction *)letmoins=fold(funi->i)(-)(* profondeur de l'arbre *)letdepth=fold(funi->0)(fundd'->1+maxdd')(* conversions en chaîne de caractères *)letshow=foldstring_of_int(funss'->Printf.sprintf"(op %s %s)"ss')letshow_p=foldstring_of_int(funss'->Printf.sprintf"(%s + %s)"ss')letshow_m=foldstring_of_int(funss'->Printf.sprintf"(%s - %s)"ss')end
Maintenant, on passe à l'encodage de Bohem-Berarducci. L'idée est de faire du type exp une « linéarisation » de l'arbre d'éxecution du fold de l'AST précédent. La fonction fold avait pour type (int -> 'a) -> ('a -> 'a -> 'a) -> exp -> 'a, le nouveau type sera donc :
typeexp={expi:'a.(int->'a)->('a->'a->'a)->'a}
Le champ expi prend deux fonctions f et g et renvoie un objet de type 'a qui constitue l'interprétation de l'expression pour les fonctions f et g, comme le faisait le fold pour l'AST.
On retrouve ensuite nos smart constructors qui mime les deux branches du fold :
La seule différence notable est dans le cas de op ou l'expression fold f g e devient e f g, étant donné que e est son « propre » fold et n'a pas besoin d'être rementionné comme argument.
Pour les différentes interprétations c'est identique, en remplaçant fold par le champ expi du type des expressions; et on obtient le module :
moduleBohem=struct(* le type des expressions est une linéarisation de son propre fold *)typeexp={expi:'a.(int->'a)->('a->'a->'a)->'a}(* smart constructors *)letlitn={expi=(funfg->fn)}letop{expi=e}{expi=e'}={expi=funfg->g(efg)(e'fg)}(* interprétation en tant qu'addition *)letplus{expi=e}=e(funi->i)(+)(* interprétation comme soustraction *)letmoins{expi=e}=e(funi->i)(-)(* profondeur de l'arbre *)letdepth{expi=e}=e(funi->0)(fundd'->1+maxdd')(* conversions en chaîne de caractères *)letshow{expi=e}=estring_of_int(funss'->Printf.sprintf"(op %s %s)"ss')letshow_p{expi=e}=estring_of_int(funss'->Printf.sprintf"(%s + %s)"ss')letshow_m{expi=e}=estring_of_int(funss'->Printf.sprintf"(%s - %s)"ss')end
L'intérêt que je vois de prime abord et le côté récursif terminal des évaluations dans cette encodage ce qui permet d'éviter des stackoverflow sur des arbres grands ou fortement déséquilibrés. Pour ce qui est des performances, j'ai fait un benchmark du pauvre en le comparant à l'approche par AST et la méthode AST mais avec un fold récursif terminal en appliquant la transformation CPS décrite ici par gasche, ce qui donne ce module :
moduleAstk=struct(* l'AST du langage à un opérateur binaire sur les entiers *)typeexpr=|Litofint|Opofexpr*expr(* smart constructors *)letlitn=Litnletopee'=Op(e,e')(* fold en CPS via CPS conversion trick *)letfoldfge=letrecloopek=matchewith|Litn->k(fn)|Op(e,e')->loope(funie->loope'(funie'->k(gieie')))inloope(fune->e)(* interprétation en tant qu'addition *)letplus=fold(funn->n)(+)(* interprétation en tant que soustraction *)letmoins=fold(funn->n)(-)(* profondeur de l'arbre *)letdepth=fold(funn->0)(fundd'->1+maxdd')(* conversions en chaîne de caractères *)letshow=foldstring_of_int(funss'->Printf.sprintf"(op %s %s)"ss')letshow_p=foldstring_of_int(funss'->Printf.sprintf"(%s + %s)"ss')letshow_m=foldstring_of_int(funss'->Printf.sprintf"(%s - %s)"ss')end
Pour le pseudo-bench cela donne :
(* la liste des entiers [1; ...; n] *)letrangen=letrecloopaccn=ifn=0thenaccelseloop(n::acc)(predn)inloop[]n;;(* op (lit 0) (op (lit 1) op(... (lit n)) *)letastn=letopenAstinList.fold_left(funei->ope(liti))(lit0)(rangen);;letbohemn=letopenBoheminList.fold_left(funei->ope(liti))(lit0)(rangen);;letastkn=letopenAstkinList.fold_left(funei->ope(liti))(lit0)(rangen);;(* la fonction de mesure approximative du temps de calcul *)lettimef=fun()->letbefore=Unix.gettimeofday()infori=1to100dof()done;letafter=Unix.gettimeofday()inafter-.before;;(* trois mesures pour se faire une idée *)time(fun()->Ast.plus(ast100_000))();;-:float=3.21051192283630371time(fun()->Bohem.plus(bohem100_000))();;-:float=4.11348295211792time(fun()->Astk.plus(astk100_000))();;-:float=5.08448696136474609
Il reste encore à investiguer sur les cas où cet encodage offre un avantage sur la méthode usuelle.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.
[^] # Re: C'est bien la peine !
Posté par kantien . En réponse au journal EDSL et F-algèbres. Évalué à 2.
J'ai joué un peu avec le code, surtout l'encodage de Bohem-Berarducci, pour comprendre un peu ce qu'il faisait et comment cela marchait. Je vais l'illustrer sur un langage simple : un opérateur binaire sur les entiers (un semi-groupe pour les adeptes de l'algèbre ;-).
On commence de manière classique avec un type
exppour l'AST du langage :on se donne des smart constructors pour notre langage et une fonction
foldgénérique sur son AST :à partir de là, on peut définir tout un tas d'interprétations différentes de l'AST et on colle le tout dans un module
Ast:Il s'utilise simplement dans une boucle REPL :
Maintenant, on passe à l'encodage de Bohem-Berarducci. L'idée est de faire du type
expune « linéarisation » de l'arbre d'éxecution dufoldde l'AST précédent. La fonctionfoldavait pour type(int -> 'a) -> ('a -> 'a -> 'a) -> exp -> 'a, le nouveau type sera donc :Le champ
expiprend deux fonctionsfetget renvoie un objet de type'aqui constitue l'interprétation de l'expression pour les fonctionsfetg, comme le faisait lefoldpour l'AST.On retrouve ensuite nos smart constructors qui mime les deux branches du
fold:La seule différence notable est dans le cas de
opou l'expressionfold f g edeviente f g, étant donné queeest son « propre » fold et n'a pas besoin d'être rementionné comme argument.Pour les différentes interprétations c'est identique, en remplaçant
foldpar le champexpidu type des expressions; et on obtient le module :Il s'utilise comme le précédent :
L'intérêt que je vois de prime abord et le côté récursif terminal des évaluations dans cette encodage ce qui permet d'éviter des stackoverflow sur des arbres grands ou fortement déséquilibrés. Pour ce qui est des performances, j'ai fait un benchmark du pauvre en le comparant à l'approche par AST et la méthode AST mais avec un
foldrécursif terminal en appliquant la transformation CPS décrite ici par gasche, ce qui donne ce module :Pour le pseudo-bench cela donne :
Il reste encore à investiguer sur les cas où cet encodage offre un avantage sur la méthode usuelle.
Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.