Introduction à la théorie des langages de programmation

TD9: Enregistrements et objets

Pour cette dernière séance nous étendons notre PCF avec des enregistrements, puis avec des constructions orientées objet. Nous travaillerons cette fois avec le module Pcf.O.

Enregistrements

Prenez connaissance de la syntaxe abstraite, définie dans Pcf.O.Ast.t. On ignorera pour l'instant les constructions orientées objet (i.e. Obj, Invoke et ObjWith), pour se concentrer sur les enregistrements. Comme dans le cours, on a la création Rec, l'accès à un champ Access et la copie/mise-à-jour CopyWith. Les étiquettes des enregistrements sont de type O.Ast.label = string. Une structure est représentée par une liste d'association de type (O.Ast.label*t) list. Par exemple l'expression PCF { n = 1 ; p = 2 } sera codée par Rec ["n",Num 1;"p",Num 2]. De même la copie/mise-à-jour { r With p = 3 } sera codée par CopyWith (Var "o", ["p",Num 3]). On adoptera la version éminement non-typée du CopyWith, qui autorise l'ajout de nouveaux champs.

Définir les types des valeurs et des environnements pour une interprétation par valeur.

Définir la fonction print_value : out_channel -> value -> unit. Il sera utile de définir aussi (mutuellement récursivement) la fonction print_values : out_channel -> (Ast.label*value) list -> unit qui affiche une liste de valeurs associées à des étiquettes, par exemple "n=1 ; p=2".

En repartant d'un interpréteur par valeur d'un TP précédent, implémenter l'interprétation par valeur de PCF étendu avec des enregistrements. Votre fonction aura le type value O.Ast.env -> O.Ast.t -> value. Elle lèvera une erreur en cas d'interprétation d'une construction orientée objet.

Nous pouvons désormais construire une boucle d'interaction: O.Top.loop print_value interpret. Testons notre nouveau langage sur quelques exemples:

PCF> let r = { n = 1 } ;;
 val r = { n=1 }
PCF> r.n ;;
 - = 1
PCF> r.m ;;
Something went wrong: OInter.Error("Cannot access field \"m\"")

PCF> { { n = 1 } with p = 2 } ;;
 - = { p=2 ; n=1 }

PCF> { { n =1 } with n = 2 } ;;
 - = { n=2 }

PCF> { { } with n = 2 } ;;
 - = { n=2 }

PCF> let incr = fun x -> { x with n = x.n+1 } ;;
 val incr = <fun>
PCF> incr { n = 0 } ;;
 - = { n=1 }

Corrigé: rec.ml.

Programmation Orientée Objet

Vous avez vu en cours comment supporter un début de Programmation Orientée Objet (POO), en traduisant trois nouvelles constructions (création d'objet, appel de méthode et copie/extension) en termes d'opérations sur les enregistrements.

Cette traduction pourrait être faite immédiatemment après la saisie de l'utilisateur. Mais ici, les trois nouvelles constructions font partie de la syntaxe abstraite: il s'agit de Obj, Invoke et ObjWith.

Par rapport au cours, on permet dans obj var { .. } et obj var { .. with .. } de ne pas préciser la variable var désignant l'objet courant, qui devra alors être this. Ainsi obj { n = 1 ; get = this.n } est équivalent à obj foo { n = 1 ; get = foo.n }. Pour refléter ce nom de variable optionnel, les constructeur Obj et ObjWith ont comme premier paramètre un objet de type string option.

Bien noter la différence entre les expressions suivantes:

Identifier notamment les liaisons de variables.

Pour interpréter les constructions orientées objet, il suffit d'interpéter leur traduction en termes d'enregistrements. On aurait pu faire ainsi dans la première partie, en interpétant à la volée Rec à partir de CopyWith:

   ...
   | Rec l -> interpret env (CopyWith (Rec []) l)
   ...

Étendre votre interpréteur aux constructions OO en utilisant cette méthode.

Testez les exemples suivants:

PCF> let o = obj self { { x=0 } with
  get = self.x ;
  step = { self with x=self.x+1 }
} ;;
 - = { step=<fun> ; get=<fun> ; x=0 }
PCF> o.get ;;
 - = <fun>
PCF> o#get ;;
 - = 0
PCF> o#step ;;
 - = { x=1 ; step=<fun> ; get=<fun> }

(* Exemple montrant la liaison "tardive".
 * On peut aussi y supprimer le a = 4. *)
PCF>
Let o =
  Obj self {
    a = 4 ;
    f = Fun y -> y + self#a ;
  }
In
  Obj s { o With a = 5 }#f 6
;;
 - = 11

PCF> let r = { n = 1 } ;;
 val r = { n=1 }
PCF> let o = obj { r with incr = { this with n = this.n+1 } } ;;
 val o = { incr=<fun> ; n=1 }
PCF> o.incr ;;
 - = <fun>
PCF> o#incr ;;
 - = { n=2 ; incr=<fun> }
PCF> o#incr#incr ;;
 - = { n=3 ; incr=<fun> }

Corrigé: obj.ml.

Bonus

On veut ajouter une liaison dans la construction obj V { R with l1=E1 ; .. ; ln=En }: désormais, dans la liste des extensions li=Ei, la variable super est implicitement liée, et désigne l'enregistrement R. Modifier la traduction de cette construction en termes d'enregistrement, et adaptez votre interpréteur. On pourra tester sur l'exemple suivant:

PCF> let o = obj { n = 0 ; incr = obj { this with n = super#n + 1 } } ;;
 val o = { n=<fun> ; incr=<fun> }
PCF> o#n ;;
 - = 0
PCF> o#incr#n ;;
 - = 1

PCF> let o =
  obj self { { x = 0 } with inc = { self with x = self.x + 1 } }
;;
(** Dans le cours, ceci est dit avec des tortues,
  * dont héritent des tortues deux fois plus rapides... *)
PCF> let extend = fun o ->
  obj self { o with inc = super.inc (super.inc self) }
;;
PCF> extend o ;;
 - = { inc=<fun> ; x=0 }
PCF> (extend o)#inc ;;
 - = { x=2 ; inc=<fun> }

Corrigé: super.ml.

Pour aller plus loin: