% category % tags types existentiels, OCaml % authors asmanur % date 1247505751.000000 % title Les types existentiels (1/2) % formatter grep -v '^%'

Pour les novendiales, notre code utilise ce qu'on appelle les types existentiels . C'est un aspect assez peu connu et assez délicat des langages fonctionnels et donc assez peu répandu. Pourtant, cette technique a des ressemblances avec ce qui peut se faire du coté de l'orienté de l'objet (en fait cela permet une encapsulation).

% endchapo

Généralités

Le quantifieur universel est le plus utilisé et le plus facilement compréhensible, il est d'ailleurs implicite dans la plupart des langages fonctionnels. Par exemple, le type de map,

('a -> 'b) -> ('a list -> 'b list)

si il devait être quantifié, donnerait

forall 'a 'b, ('a -> 'b) -> ('a list -> 'b list)

Le quantifieur universel permet donc une généricité à travers le polymorphisme paramétrique.

Formellement, si on écrit type t = forall 'a. 'a -> 'a, le type des fonctions acceptant un paramètre de n'importe quel type et renvoyant une valeur de ce même type (t est habité uniquement par la fonction identité), on voit bien que t ne dépend plus de 'a.

Le but serait donc de pouvoir écrire par analogie type t = exists 'a. 'a -> 'a. Que représente ce type ? Il représente les fonctions telles qu'il existe un type 'a tel que la fonction soit de type 'a -> 'a, la fonction n'est donc pas générique. Par exemple, (fun n -> n + 1) est un membre de type t.

L'avantage d'un tel type c'est qu'il ne dépend pas de 'a donc on peut en construire une liste.

On peut imaginer avoir un type t = exists 'a. ('a * ('a -> 'a)). Ainsi ce type représente les couples de la forme (état, transition) avec état n'importe quel type. Par exemple on pourrait avoir une liste de ce type

[(4, succ); ("foo", ( (^) "f" )] : t list

Ensuite, on pourrait définir une fonction update : t list -> t list qui à chaque couple (état, transition) associe (transition état, transition).

Cela permet ainsi de mettre à jour plusieurs chose complètement différente d'un coup — des personnages, des objets, etc. — et tout mettre à jour tout d'un coup sans se préoccuper de rien.

Implémentation

OCaml ne dispose pas de base de ces types existentiels mais il est possible de de les implémenter (avec une légère surcharge syntaxique).

En fait, en logique, on peut exprimer le quantifieur existentiel à l'aide de la négation et du quantifieur universel. On a l'identité ∃x. P(x) ≣ ¬ (∀x. ¬ P(x)) (¬ étant la négation) (il s'agit simplement d'une trivialité facile à se représenter). Si on sait trouver une représentation sympathique de la négation, en exploitant l'analogie de Curry-Howard, on pourra trouver une représentation du type exists 'a. P a. La définition usuelle de ¬P est P ⇒ ⊥ (⊥ se lisant « faux »). Sachant que la proposition fausse implique toute proposition 1, on a ¬P ≣ ∀Q. P ⇒ Q.

On finit par aboutir à un encodage du type ∀R. (∀x. P(x) ⇒ R) ⇒ R (pas strictement équivalent à l'existence ceci dit).

Intéressons nous tout d'abord, à (∀x. P(x) ⇒ R) pour un certain R. Le type équivalent serait forall 'a. 'a P -> R. Il s'agit ici du type des opérateurs agissant sur n'importe 'a P et renvoyant un R indépendant de 'a. Pour définir un tel type polymorphe en caml, une des façons de procéder est d'utiliser des records. Ainsi le type des opérateurs est

type 'R opérateur = { op : 'x. 'x P -> 'R }

Le 'x. signifie forall x..

Maintenant, on peut trouver un équivalent du type complet :

(forall R. R operateur) -> R

Soit en caml :

type t = { app : 'R. 'R opérateur -> 'R }

Un objet de ce type est un élément qui prend un opérateur en argument et l'appelle sur une donnée qui est ici cachée. On peut ainsi écrire une fonction pack qui convertit un 'x P en un type t (cachant la valeur dans une closure) :

let pack élément = { app = (fun {op = opérateur} -> opérateur élément) }

Il y a là le principe des types existentiels ; cacher un type dans une closure pour offrir une interface commune. C'est une possibilité également offerte par les langages orientés objets (et elle est nettement plus utilisée dans les langages OO ceci dit).

Rappelons que notre type que l'on veut quantifier est 'a * ('a -> 'a). Imaginons que nous souhaitons écrire une fonction next qui retourne l'état suivant. Il s'agit ici d'un opérateur que l'on peut définir ainsi :

let suivant = { op = (fun (état, transition) -> pack (transition état, transition) }

Il ne faut pas oublier d'empaqueter le résultat au risque de rencontrer une vive réaction de caml. Pour l'appliquer il suffit de faire :

(pack (4, succ)).app suivant

À propos de l'exemple : si on est sûr que l'interface ne bougera pas, on peut extrêment simplifier le problème en identifiant un objet à son interface — en faisant un record de fonctions par exemple — ceci dit si l'interface est amenée à évoluer ce n'est pas forcément pratique.

Voici le code au complet, avec une petite variation au niveau des opérateurs, afin de leur permettre de prendre un paramètre en plus, c'est bien utile.

module Make (T : sig type 'a t end) : sig
  type ('b, 'c) op = { op: 'a. 'b -> 'a T.t -> 'c }
  type t
  val pack : 'a T.t -> t
  val exec : ('b, 'c) op -> ('b -> t -> 'c)
end 
= struct
  type ('b, 'c) op = { op: 'a. 'b -> 'a T.t -> 'c }

  type t = { obj: 'b 'c. 'b -> ('b, 'c) op -> 'c }
  
  let pack obj = {
    obj = (fun x {op=op} -> op x obj)
  }

  let exec f x {obj = o} = o x f
end

Au programme de demain : des exemples d'applications dans notre projet. Stay tuned !


Notes :

[1] Il n'est pas évident que ce soit à la seule à vérifier cette propriété, mais étant donné les propriétés de ⇒, si A vérifie également cette propriété, on a immédiatement, ⊥ ⇔ A