Types sommes extensibles
OCaml ≥ 4.02.0
Table des matières
1. Introduction

Les types sommes (ou variants) extensibles permettent de définir des types sommes dont on peut enrichir la liste des constructeurs. Ils ont été introduits dans la version 4.02.0 d'OCaml.

Le type exn représentant les exceptions était déjà un tel type, puisque la définition d'une exception ajoutait un constructeur à ce type. Cependant, il s'agissait d'un traitement spécifique à ce type dans le compilateur. Le type exn est maintenant un type somme extensible "comme un autre".

2. Définition

Un type somme extensible est d'abord défini sans constructeur, en indiquant sa nature extensible, de la façon suivante:

# type t = .. ;;
type t = ..

On ajoute ensuite des constructeurs à l'aide de +=:

# type t += A of int | B of string;;
type t += A of int | B of string

Il est bien sûr possible d'avoir des types paramétrés; dans ce cas, les paramètres de types doivent être rappelés à chaque extension:

# type 'a expr = .. ;;
type 'a expr = ..
# type 'a expr += Val of 'a ;;
type 'a expr += Val of 'a
# type 'a expr += Plus of 'a expr * 'a expr ;;
type 'a expr += Plus of 'a expr * 'a expr

Il est possible d'étendre un type qui n'est pas dans le même module, en indiquant le chemin du type au lieu du simple identifiant utilisé dans une nouvelle définition:

# module M = struct type 'a expr = .. end ;;
module M : sig type 'a expr = .. end
# type 'a M.expr += (* noter le chemin du type étendu *)
  | Plus of 'a expr * 'a expr
  | Moins of 'a expr * 'a expr ;;
type 'a M.expr += Plus of 'a expr * 'a expr | Moins of 'a expr * 'a expr

Lorsqu'un constructeur est ajouté, sa portée est la même que les constructeurs d'un type somme non extensible ou un champ de type enregistrement (cf. 3).

Enfin, il est possible de redéfinir un constructeur déjà présent dans le type, afin de lui donner une portée supplémentaire, comme s'il était défini ici:

# module M = struct
  type 'a expr = ..
  type 'a expr +=
    | Val of 'a
    | Plus of 'a expr * 'a expr
end;;
module M :
  sig
    type 'a expr = ..
    type 'a expr += Val of 'a | Plus of 'a expr * 'a expr
    
  end
# module N = struct
  type 'a M.expr +=
    | Plus = M.Plus (* Redéclaration du constructeur M.Plus *)
    | Moins of 'a M.expr * 'a M.expr
  end;;
module N :
  sig
    type 'a M.expr +=
        Plus of 'a M.expr * 'a M.expr
      | Moins of 'a M.expr * 'a M.expr
    
  end
Avertissement 1: Constructeurs de même nom

Il est possible d'étendre un type extensible avec deux constructeurs portant le même nom:

# type t = .. ;;
type t = ..
# type t += A of int ;;
type t += A of int
# type t += A of float ;;
type t += A of float

Bien sûr, cela ne facilite pas la lecture du code. De plus, le premier constructeur A dans l'exemple ci-dessus n'est plus utilisable après la définition du second. On peut cependant étendre un type avec des constructeurs identiques dans deux modules différents, chacun ayant une signification dans le module qui le définit:

# type t = .. ;;
type t = ..
# module M1 = struct type t += A of int end  ;;
module M1 : sig type t += A of int end
# module M2 = struct type t += A of string end ;;
module M2 : sig type t += A of string end
3. Usage

La référence aux constructeurs d'un type somme extensible est faite de la même façon que pour les types sommes, que cela soit pour la construction ou le filtrage de valeurs.

# let v = M.Val 3;;
val v : int M.expr = M.Val <poly>
# let op = M.Plus (v, v);;
val op : int M.expr = M.Plus (M.Val <poly>, M.Val <poly>)
# let op2 = M.Plus (v, v);;
val op2 : int M.expr = M.Plus (M.Val <poly>, M.Val <poly>)
# let op3 = N.Moins (v, v);;
val op3 : int M.expr = N.Moins (M.Val <poly>, M.Val <poly>)

Attention, puisque le constructeur Moins n'est déclaré que dans le module N, on ne peut y faire référence en l'appelant M.Moins, bien que le type expr soit bien défini dans M. La portée d'un constructeur dépend de l'endroit où il est défini:

# let op4 = M.Moins (v, v);;
Error: Unbound constructor M.Moins

Si nous pouvons faire référence à N.Plus, c'est parce que ce constructeur a été redéclaré dans N en utilisant la notation

type 'a M.expr +=
  | Plus = M.Plus

Comme ces types sont susceptibles d'être enrichis de nouveaux constructeurs, le filtrage des valeurs de tels types doit comporter un cas "attrape-tout", sinon le compilateur émet un avertissement indiquant que le filtrage n'est pas exhaustif:

# let rec to_int = function
| M.Val n -> n
| M.Plus (v1, v2) -> (to_int v1) + (to_int v2)
| N.Moins (v1, v2) -> (to_int v1) - (to_int v2);;
val to_int : int M.expr -> int = <fun>
Avertissement 2:
Un bug dans OCaml 4.03.0 a pour effet de ne pas afficher cet avertissement.

En effet, si nous étendons notre type avec un constructeur supplémentaire, et que nous passons à notre fonction to_int une valeur construite avec ce constructeur, nous obtenons une erreur de filtrage:

# type 'a M.expr += Mult of 'a M.expr * 'a M.expr ;;
type 'a M.expr += Mult of 'a M.expr * 'a M.expr
# to_int (Mult (v, v)) ;;
- : int = 0
4. Exemple

Un cas d'utilisation des types sommes extensibles est la manipulation de valeurs d'un type dont les possibilités de constructions dépendent des modules présents à la compilation, ou pour composer (multiplexer/démultiplexer) des valeurs dont une partie du traitement est commun.

Prenons l'exemple de messages dans une file. Bien que le traitement de la file soit commun, les messages présents dans la file pourront varier selon qu'on lie avec certains modules ou non.

Voici la partie commune de traitement de la file, avec la définition du type extensible ainsi qu'une fonction permettant d'enrichir le traitement des valeurs de ce type pour supporter les nouveaux constructeurs:

module Common = struct
  type msg = ..

  let handle_msg = ref (function _ -> failwith "Unable to handle message")

  let extend_handle f =
    let old = !handle_msg in
    handle_msg := f old

  let (q : msg Queue.t) = Queue.create ()
  let add msg = Queue.add msg q
  let handle_queue_messages () = Queue.iter !handle_msg q
  
end;;

Nous pouvons ensuite définir deux modules, chacun ajoutant des constructeurs de messages et utilisant la fonction extend pour permettre à handle_queue_messages de traiter ces nouveaux messages. De plus, ces modules mettent eux-mêmes des messages dans la file. Il serait bien sûr possible de créer des messages à l'extérieur de ces modules, en référençant leurs constructeurs par M1.Reload par exemple. Mais dans le cas de modules chargés dynamiquement ou liés ou non à la compilation selon la configuration, on peut imaginer que ces modules fassent des traitements opaques, tout en s'inscrivant dans la gestion de messages de la file.

module M1 = struct
  type Common.msg += Reload of string | Alert of string

  let handle fallback = function
  | Reload s -> print_endline ("Reload "^s)
  | Alert s -> print_endline ("Alert "^s)
  | x -> fallback x

  let () = Common.extend_handle handle
  let () = Common.add (Reload "config.file")
  let () = Common.add (Alert "Initialisation done")
end
module M2 = struct
  type Common.msg += Send of string * string

  let handle fallback = function
  | Send (dest, msg) ->
      print_endline (Printf.sprintf "Send (%s, %s)" dest msg)
  | x -> fallback x

  let () = Common.extend_handle handle
  let () = Common.add (Send ("client", "config reloaded"))

end;;

Le traitement des messages se fait ensuite naturellement:

# let () = Common.handle_queue_messages ();;
Reload config.file
Alert Initialisation done
Send (client, config reloaded)

Si l'on souhaite charger dynamiquement les modules M1 et M2, comme des greffons ajoutant des fonctionnalités via des messages, on peut placer le contenu des modules Common, M1 et M2 respectivement dans les fichiers common.ml, m1.ml et m2.ml, et ajouter par exemple un fichier load.ml avec le contenu suivant:

Dynlink.loadfile "m2.cmo";;
Dynlink.loadfile "m1.cmo";;
let () = Common.handle_queue_messages ();;

Ensuite, on compile les modules et on les lie ensemble dans un exécutable load.x:

ocamlc -c common.ml
ocamlc -c m1.ml
ocamlc -c m2.ml
ocamlc -o load.x dynlink.cma common.cmo load.ml

A l'exécution de load.x, les modules M1 et M2 sont chargés dynamiquement, étendent le type msg, étendent la fonction de traitement des messages et ajoutent chacun des messages dans la file, qui sont ensuite traités après le chargement dynamique:

./load.x
Send (client, config reloaded)
Reload config.file
Alert Initialisation done