Programmation Avancée
TP1: Modules
Aujourd'hui, un exercice pour apprendre à maîtriser l'essentiel du système de modules d'OCaml, puis une application pratique de la chose.
Partie I: Conteneurs
On considère la signature suivante, définissant des conteneurs (type t
)
pour un certain type d'éléments (type elt
).
module type Container = sig type t type elt val empty : t val add : elt -> t -> t val merge : t -> t -> t val member : elt -> t -> bool val fold : ('a -> elt -> 'a) -> 'a -> t -> 'a end
La spécification informelle sera que t
se comporte comme un multi-ensemble,
empty
étant le multi-ensemble vide, add
l'ajout d'un élément et merge
la fusion de deux multi-ensembles. La fonction fold
doit réaliser une itération
sur tous les éléments d'un multi-ensemble, dans un ordre non spécifié.
Par exemple, fold f x0 {e1,e2,e3} = f (f (f x0 e3) e2) e1
.
Question 1
Définissez une implémentation du type Container
par des listes.
Dans ce cas on n'aura besoin d'aucune opération spécifique sur le type
des éléments: on réalisera donc notre implémentation comme un foncteur
LContainer
de AnyT
vers Container
avec la déclaration
module type AnyT = sig type t end
Question 2
Définir une implémentation par des listes triées, et exploiter l'ordre
pour tester l'appartenance. Comme on a besoin d'un ordre sur le type
elt
, on va cette fois écrire un foncteur SLContainer
de
Set.OrderedType
(cf. bibliothèque standard) vers Container
.
Testez vos deux implémentations, au moins avec le code suivant:
let () = let module Test (M:Container with type elt = int) = struct open M let () = let s = add 42 (add 16 (add 64 empty)) in let s = merge s s in assert (member 42 s) ; assert (member 16 s) ; assert (member 64 s) ; Printf.printf "Result: " ; fold (fun () elt -> Printf.printf "%d+" elt) () s ; Printf.printf "ø\n" end in let module A = Test(LContainer(Int)) in let module B = Test(SLContainer(Int)) in ()
Dans la vraie vie
La première implémentation n'est pertinente en pratique que si
on n'a pas d'ordre valable sous la main.
La seconde implémentation est meilleure mais sous-optimale: on lui préfèrera
des arbres binaires de recherche. En pratique, on utilisera souvent
les modules Set
, Map
et Hashtbl
de la bibliothèque standard.
Question 3
On introduit une signature pour les types affichables:
module type Printable = sig type t val to_string : t -> string end
Définir la signature PContainer
qui étend la signature Container
avec un champ to_string : t -> string
.
Définir un foncteur MakePrintable
qui, étant donnés un ensemble d'éléments affichables
et une instance de Container
sur ces éléments, renvoie une instance
de PContainer
.
Tester sur le code suivant:
let () = let module Test (M:PContainer with type elt = string) = struct open M let () = let s = add "d" (merge (add "a" empty) (add "c" (add "b" empty))) in Printf.printf "Result: %s\n" (to_string s) end in let module A = Test(MakePrintable(String)(LContainer(String))) in let module B = Test(MakePrintable(String)(SLContainer(String))) in ()
Ajouter un test où l'on construit, puis affiche un conteneur (non trié)
de conteneurs (trié) de chaînes de caractères.
On devra utiliser MakePrintable
deux fois, d'abord pour rendre les
conteneurs de chaînes affichables, puis pour rendre les conteneurs
de conteneurs affichables.
Question 4
Sur le papier, le système de modules garantit l'abstraction. Un foncteur qui reçoit en argument un module dont un type est abstrait ne pourra jamais accéder aux éléments de ce type autrement que par les fonctions fournies par le module. Il ne peut donc observer l'implémentation du type abstrait que par le biais des fonctions fournies explicitement.
En pratique, cela n'est pas tout à fait vrai: la fonction d'égalité
(=) : 'a -> 'a -> bool
permet de comparer des valeurs de tout type,
y compris un type abstrait. (On notera que cette fonction ne peut être
programmée en OCaml: elle est définie côté C dans le runtime
du langage. Les fonctions polymorphes définissables dans le langage,
comme par exemple List.mem
, ne brisent pas l'abstraction.)
Etendre le premier foncteur Test
en utilisant l'égalité polymorphe
pour tester si M:Container
est instantié par LContainer
ou SLContainer
.
Pensez-vous que cela soit un problème en pratique?
Partie II: Visualisation d'arbres
Nous allons maintenant jouer avec une application créée par Alexandre Miquel, qui utilise la géométrie hyperbolique pour afficher de gros arbres.
Récupérez les sources de l'application.
L'archive est incomplète: cela ne compilera pas tout de suite.
Elle contient un Makefile
qui décrit comment compiler
mli
et ml
et comment lier
le tout avec les bibliothèques requises pour obtenir une application.
Le Makefile inclut aussi un fichier de dépendances entre fichiers sources,
(re)généré automatiquement à l'aide d'ocamldep
.
Si tout ceci n'est pas déja familier, jetez-y un oeil à un moment ou un
autre, si besoin en s'aidant de la
documentation.
Question 1
L'application n'impose pas un type de données particulier, mais fonctionne pour tout type d'arbre satisfaisant la signature suivante:
module type TREE = sig type t type label val children : t -> t list val label : t -> label val string_of_label : label -> string end
Votre objectif est de créér un fichier mytree.ml
satisfaisant l'interface
TREE
avec en plus un champ val root : t
correspondant à
l'arbre à afficher au lancement de l'application.
Commencez avec un type d'arbres classique, donné par un type variant récursif.
Vous pouvez maintenant compiler et exécuter: make
, ./htree.opt
.
Utilisez la touche q
pour quitter l'application.
Question 2
Exploitez la flexibilité de la signature TREE
pour définir des
arbres très grands voire infinis, générés paresseusement: on pourra décrire
l'arbre de tous les mots, l'arborescence du système de fichiers, du web,
l'arbre sémantique d'une formule, etc.
Si vous avez plusieurs implémentations, modifiez le Makefile
et le
module Main
pour utiliser vos nouveaux modules, par exemple en fonction
d'options passées sur la ligne de commande
de l'application (Sys.argv
, cf
doc).
Références
Je recommande l'article d'Alexandre Miquel sur son afficheur d'arbres. Le code source complet est aussi disponible.