Salut,
résumé : pour des besoins personnels, j'ai changé du code dans la bibliothèque standard Map. Comme je connais mal l'algorithmique de cette structure de donnée (des arbres de données équilibrés), et que je sais que certains d'entre vous sont très au courant, je viens vous raconter ma vie, montrer du code, et je voudrais votre avis.
Dans le contexte du projet Batteries, je travaille en ce moment sur une factorisation des modules Map.Make (Map fonctorielles) et PMap (map polymorphiques).
Les fonctions de ces modules utilisent deux informations, la fonction de comparaison et l'arbre (la structure de donnée sous-jacente). Map a la fonction de comparaison comme constante, et accède directement à l'arbre, et PMap prend en argument un record contenant un champ pour l'arbre et un champ pour la fonction et, bien sûr, renvoie des valeurs de la même forme.
Pour factoriser, je regroupe les implémentations des fonctions dans un module concret qui prend et renvoie directement l'arbre, mais aussi, quand c'est nécessaire, la fonction de comparaison. Ensuite j'ai deux "écorces", une pour Map.Make et une pour PMap, qui passent les paramètres qu'il faut, construise et déconstruisent les données qu'il faut.
L'état actuel du module :
http://github.com/gasche/batteries-i.../src/batMap.ml
L'intérêt est qu'il est ensuite facile de s'assurer que Map et PMap proposent les mêmes fonctionnalités, et de transmettre à l'un les améliorations de l'autre. J'espère aussi me débarasser du hack "on révèle l'implémentation des Map stdlib en cassant le typage" utilisé jusqu'à présent pour ajouter des fonctions au module Map.Make (issu de la bibliothèque standard).
Dans le module Concrete, certaines fonctions ont besoin de l'opération de comparaison, typiquement la fonction "find", et d'autres n'en ont pas besoin, typiquement la fonction "iter". On pourrait croire que c'est arbitraire, mais il y a en fait une logique interne claire à cette distinction : si la fonction a besoin de trouver un chemin dans l'abre en fonction d'une clé, il lui faut comparer, sinon elle ne devrait pas en avoir besoin. iter ne cherche aucune clé en particulier, elle itère sur toutes les clés. De même, min_binding qui renvoie la plus petite clé n'en a pas besoin, il lui suffit de descendre tout à gauche dans l'arbre.
Dans le but de porter la fonction "split" ajoutée dans le module standard Map depuis la 3.12, je m'intéresse à la fonction auxiliaire "join" qu'elle utilise. Dans map.ml (de la distribution OCaml) :
Join sert à réunir une branche gauche, une branche droite, et au milieu une clé et une valeur, mais dans un cas où on ne sait pas si les deux branches sont déséquilibrées. Si c'est le cas, il faut ré-équilibrer l'arbre en utilisant l'opération `bal`.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10 (* Same as create and bal, but no assumptions are made on the relative heights of l and r. *) let rec join l v d r = match (l, r) with (Empty, _) -> add v d r | (_, Empty) -> add v d l | (Node(ll, lv, ld, lr, lh), Node(rl, rv, rd, rr, rh)) -> if lh > rh + 2 then bal ll lv ld (join lr v d r) else if rh > lh + 2 then bal (join l v d rl) rv rd rr else create l v d r
Mon soucis avec cette implémentation est qu'elle utilise, dans le cas où l'une des branches est vide, la fonction `add`. C'est la fonction générique d'ajout d'un élément dans l'arbre, et elle demande (dans mon implémentation) la fonction de comparaison. Si je reprends cette implémentation, je dois donc demander pour `join` une fonction de comparaison :
Ce n'est pas très grave mais ça casse la séparation globale "besoin de cmp ou pas ?". join utilise cmp alors qu'elle n'a besoin de trouver la position d'aucun élément en particulier. En effet, on sait en fait que la clé `v` est plus petite que toutes les clés de la branche droite, et plus grande que toutes celles de la branche gauche. Il suffirait donc d'écrire :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 let rec join cmp l v d r = match (l, r) with (Empty, _) -> add v d cmp r | (_, Empty) -> add v d cmp l | (Node(ll, lv, ld, lr, lh), Node(rl, rv, rd, rr, rh)) -> if lh > rh + 2 then bal ll lv ld (join cmp lr v d r) else if rh > lh + 2 then bal (join cmp l v d rl) rv rd rr else create l v d r
Qu'en pensez-vous ? Est-ce que le code vous paraît correct ?
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 (* beware : those two functions assume that the added k is *strictly* smaller (or bigger) than all the present keys in the tree; it does not test for equality with the current min (or max) key. Indeed, they are only used during the "join" operation which respects this precondition. *) let rec add_min_binding k v = function | Empty -> singleton k v | Node (l, x, d, r, h) -> bal (add_min_binding k v l) x d r let rec add_max_binding k v = function | Empty -> singleton k v | Node (l, x, d, r, h) -> bal l x d (add_max_binding k v r) (* Same as create and bal, but no assumptions are made on the relative heights of l and r. *) let rec join l v d r = match (l, r) with (Empty, _) -> add_min_binding v d r | (_, Empty) -> add_max_binding v d l | (Node(ll, lv, ld, lr, lh), Node(rl, rv, rd, rr, rh)) -> if lh > rh + 2 then bal ll lv ld (join lr v d r) else if rh > lh + 2 then bal (join l v d rl) rv rd rr else create l v d r
PS : il y aurait un énorme hack qui marcherait dans mon cas, qui serait de garder les appels à add en leur passant une "fausse" fonction de comparaison, qui vaudrait toujours 1 ou toujours -1 selon que l'on veut ajouter le minimum ou le maximum. Mais c'est moche, c'est fragile et je ne l'aime pas.
PPS : si l'implémentation vous semble conclusive, je l'enverrai peut-être sur le bug-tracker Caml, parce que comme la fonction de comparaison est user-defined et potentiellement coûteuse ça pourrait les intéresser d'avoir une implémentation qui en minimise les appels.
Partager