Dans un développement logiciel récent, j'ai un type représentant
des documents, ces derniers étant "évalués" vers du XML (du XHTML plus précisément).
Cette évaluation nécessite un contexte, par exemple pour la numérotation
des numéros de sections. Enfin, l'évaluation n'est pas faite d'un seul
coup, mais en plusieurs étapes: le serveur évalue tout ce qu'il peut
pour obtenir un document XML avec des "trous" qu'il envoie au client.
Puis il envoie les morceaux manquants quand il sont prêts. De son côté, le client
peut changer des paramètres (via un formulaire) dans un morceau du document et
demander sa réévaluation avec ces nouveaux paramètres.
Et là, c'est le drame. Il faut un moyen au serveur de connaître
le contexte à utiliser pour évaluer ce morceau de document.
En fait, rien de compliqué, il suffit de passer par une représentation
du contexte en string, pour que le client le renvoie au
serveur1.
Là où ça se complique, c'est que ces informations de contexte
sont extensibles. Le type représentant les document est un
type variant
ouvert, auquel on peut ajouter des constructeurs.
Cela permet d'enrichir la représentation des documents, pourvu
qu'on donne une traduction vers XML.
Ces fonctions de traduction nécessitent (éventuellement) des
informations de contexte, ce dernier doit donc naturellement pouvoir
être enrichi lui aussi.
Enfin, comme il s'agit de développer un cadriciel, on aura la possibilité
d'ajouter des représentations de documents (et donc des valeurs au contexte)
par greffons ou bien en compilant ou pas certains modules.
On a donc en gros et de façon très simplifiée quelque chose comme:

type doc = .. val doc_to_xml_ref : (doc -> ctx -> ctx * xml) ref val doc_to_xml : doc -> ctx -> ctx * xml
Le type doc étant extensible, lorsqu'on lui ajoutera un constructeur,
on modifiera la fonction doc_to_xml_ref pour traiter ce constructeur,
et appeler l'ancienne fonction pour les autres constructeurs.
Bref, ce qui nous intéresse ici est le type ctx représentant le
contexte d'évaluation des documents, documents évidemment récursifs; le
contexte est donc passé (et parfois modifié) récursivement lors de cette
évaluation.
Une solution simple serait de définir le contexte comme un map de
string vers string, les clés et les valeurs étant
forcément des string, et éventuellemen parser les valeurs
si elles contiennent une information un minimum structurée.
Mais nous sommes ici en OCaml, pas en Perl ou PHP. Nous avons un vrai système
de types, alors profitons-en.
Pour définir ce type ctx, l'idée est d'utiliser la même technique
que pour la représentation des documents, à savoir deux types extensibles,
l'un pour les clés, l'autre pour les valeurs associées dans un map:

module type Ctx = sig type key = .. type value = .. type ctx val empty : ctx val get : ctx -> key -> value option val set : ctx -> key -> value -> ctx end;; module C : Ctx = struct type key = .. type value = .. module M = Map.Make (struct type t = key let compare = Stdlib.compare end) type ctx = value M.t let empty = M.empty let get ctx k = try Some (M.find k ctx) with Not_found -> None let set ctx k v = M.add k v ctx end;;
Nous nous contentons d'utiliser la fonction Pervasives.compare
pour avoir un ordre sur le type des clés. Nous pourrions aussi définir
notre propre fonction, mais il faudrait pouvoir l'enrichir lorsque de
nouveaux constructeurs sont ajoutés, ce qui compliquerait les choses et
à priori Pervasives.compare suffira.
Nous pouvons maintenant nous servir de notre structure pour définir
un nouveau constructeur de clés et un nouveau constructeur de valeurs:
Ensuite, get et set permettent de récupérer ou ajouter
une association clé/valeur dans le contexte:

# let ctx = C.set C.empty Section_path (Path [1 ; 2 ; 1]) ;; val ctx : C.ctx = <abstr> # match C.get ctx Section_path with Some (Path path) -> path | _ -> assert false;; - : int list = [1; 2; 1]
Bien sûr, la définition de nouveaux constructeurs peut être faite dans des
modules où se restreint leur utilisation, dans notre cas ce serait dans le
même module qui enrichit le type doc et qui utilise des associations
clés/valeurs du contexte pour l'évaluation vers du XML.
Il est même possible (c'est ce que je fais dans le code en question) de paramétrer
ces modules par les modules définissant le type doc et le type ctx.
Cela donne quelque chose de la forme:
Comme dit plus haut, j'ai également besoin d'une représentation du contexte sous forme
de string, pour passer au client qui la redonnera au serveur lors d'une
demande d'évaluation d'un morceau de document.
Pour cela, j'ajoute deux fonctions to_string et of_string à la signature
Ctx, et j'utilise l'extension de syntaxe
ppx_deriving_yojson,
permettant
de générer les fonctions de lecture et écriture en JSON de n'importe quel type de données
OCaml:

module type Ctx = sig type key = .. [@@deriving yojson] type value = .. [@@deriving yojson] type ctx val empty : ctx val get : ctx -> key -> value option val set : ctx -> key -> value -> ctx val to_string : ctx -> string val of_string : string -> ctx end =
Pour les types extensibles, le code généré fournit deux fonctions
(..._of_yojson et ..._to_yojson) qui échouent
mais surtout il fournit également de quoi étendre ces fonctions pour traiter les nouveaux
constructeurs2.
Mon implémentation du contexte devient alors la suivante. Je dois définir un
type assoc_list annoté par [@@deriving yojson] pour obtenir
des fonctions de (dé)sérialisation d'une liste de paires (key, value),
car l'extension ppx_deriving_yojson ne gère pas encore les maps. Pour obtenir
une représentation de mon contexte en string, je transforme donc
le map en liste de paires et j'utilise le code généré par l'extension pour
obtenir une chaîne (contenant une représentation JSON). Pour la lecture
depuis une chaîne, les opérations inverses sont effectuées: lecture du JSON
puis appel de la fonction transformant ce JSON en liste de paires et reconstruction
du map. Si la chaîne est syntaxiquement incorrecte ou que le JSON ne correspond
pas à mon type de données, je retourne un contexte vide3:

module C : Ctx = struct type key = .. [@@deriving yojson] type value = .. [@@deriving yojson] type assoc_list = (key * value) list [@@deriving yojson] module M = Map.Make (struct type t = key let compare = Pervasives.compare end) type ctx = value M.t let empty = M.empty let get ctx k = try Some (M.find k ctx) with Not_found -> None let set ctx k v = M.add k v ctx let map_to_list = let f = M.fold (fun k v acc -> (k, v) :: acc) in fun map -> f map [] let map_of_list = List.fold_left (fun map (k,v) -> M.add k v map) empty let to_string ctx = Yojson.Safe.to_string (assoc_list_to_yojson (map_to_list ctx)) let of_string str = match assoc_list_of_yojson (Yojson.Safe.from_string str) with | exception _ -> empty | `Ok map -> map_of_list map | `Error _ -> empty end
Dans les modules où les types key et value sont enrichis par
de nouveaux constructeurs, il suffit d'ajouter [@@deriving yojson]
pour que la (dé)sérialisation de ces nouveaux constructeurs soit utilisable
par les fonctions assoc_list_to_yojson et assoc_list_of_yojson
ci-dessus (générées par l'extension ppx):
module My_ctx (D: Doc) (C : Ctx) = struct ... type C.key += Mon_option [@@deriving yojson] type C.value += Int of int [@@deriving yojson] ... end
Il serait sans doute possible d'utiliser une autre extension permettant la
génération des fonctions de (dé)sérialisation. Cependant, je n'ai pas pris le temps
de regarder lesquelles supportent les types extensibles (j'ai dû ajouter le support
des types extensibles à ppx_deriving_yojson, il n'y était pas au départ).
Comme je compte utiliser la sérialisation JSON dans le reste de l'application,
ça me va tout à fait.
Si d'aventure des problèmes de performace apparaissaient
à cause de cette sérialisation, et qu'une autre extension offrait les mêmes
possibilités dans un autre format, il me suffirait de changer d'extension et les
fonctions qui y font appel dans ma définition de Ctx,
tout le reste ne changeant pas.