Types abstraits de données - Approche fonctionnelle et approche objet
Table of Contents
1 Consignes - Remarques préalables
Après avoir créé un projet Java ModTyp16_eval, y importer à la racine l'archive (Import > General > Archive File > Zip à sélectionner). Développer ensuite dans les paquets indiqués ci-dessous. Finalement, exporter votre projet (Export > General > Archive File) puis déposer sur Campus.
Remarque
- Le langage de programmation utilisé est Java, version 8.
- On utilise particulièrement le type Function <E, F> du paquet java.util.function. Il désigne les fonctions de E vers F. Il existe une méthode apply pour appliquer la fonction à un argument et une méthode compose pour la composition. Enfin, on peut définir une fonction avec la notation (x -> expression).
- On utilise aussi le type Predicate <E> du paquet java.util.function. Il désigne les prédicats sur E. Il possède une méthode test pour vérifier la validité du prédicat.
2 Un type abstrait de données pour les multi-ensembles
On cherche à implémenter des multi-ensembles suivant l'approche fonctionnelle puis l'approche objet. Avec l'approche fonctionnelle, on définit
- une structure de données élémentaires pour la représentation de multi-ensembles,
- un module définissant les opérations disponibles pour le type abstrait et dépendant d'une représentation,
- une implémentation du module en utilisant la structure de données comme représentation.
Avec l'approche objet, on définit
- une interface intégrant les méthodes associées à la représentation des données et les opérations du module,
- une implémentation de l'interface.
Les multi-ensembles sont considérés comme un type abstrait de données muni des opérations suivantes. On note ME le type des multi-ensembles, E le type des éléments contenus dans le multi-ensemble, boolean le type des booléens, int le type des entiers relatifs, String le type des chaînes de caractères (comme en Java), x le produit cartésien et -> le type flèche (des fonctions).
// Constructeurs vide : ME cons : E x ME -> ME // Sélecteur estVide : ME -> boolean estCons : ME -> boolean estApplication : ME -> boolean estSomme : ME -> boolean // Destructeurs element : ME -> E reste : ME -> ME support : ME -> ME fonction : E -> E gauche : ME -> ME droit : ME -> ME // Foncteur taille : ME -> int application : ME x (E -> E) -> ME filtrage : ME x (E -> boolean) -> ME // Monoïde additif zero : ME somme : ME x ME -> ME // Egalité egal : ME x ME -> boolean // Représentation representation : ME -> String
A ces opérations sont associées de nombreuses équations, tout à fait habituelles. En voici des exemples. La composition fonctionnelle est notée o.
cons(element(a), reste(a)) = a si (estVide a) = false support(application(a, f)) = a fonction(application(a, f)) = f application(vide, f) = vide application(cons(e, a), f) = cons(f(e), application(a, f)) application(somme(a, b), f) = somme(application(a, f), application(b, f)) application(application(a, f), g) = application(a, g o f) filtrage(vide, P) = vide filtrage(cons(e, a), P) = cons(e, filtrage(a, P)) si P(e) = true filtrage(cons(e, a), P) = filtrage(a, P) si P(e) = false filtrage(somme(a, b), P) = somme(filtrage(a, P), filtrage(b, P)) filtrage(application(a, f), P) = filtrage(a, P o f) zero = vide gauche(somme(a, b)) = a droit(somme(a, b)) = b somme(vide, a) = a somme(cons(e, a), b) = cons(e, somme(a, b)) somme(somme(a, b), c) = somme(a, somme(b, c))
En revanche, les équations valables pour une liste ne tiennent pas toujours.
element(cons(e, a)) = e ? reste(cons(e, a)) = a ?
Un multi-ensemble peut en effet réordonner les éléments. Pour les implémentations considérées ci-dessous, ces équations seront cependant valides.
Pour implémenter les multi-ensembles, on utilisera le patron de conception "Composite", qui traduit directement la définition inductive suivante : un multi-ensemble est
- soit vide
- soit un multi-ensemble construit à partir d'un élément et d'un multi-ensemble,
- soit l'application d'une fonction à un multi-ensemble,
- soit la somme (union) de deux multi-ensembles.
En choisissant cette définition inductive, on opte pour une définition paresseuse des opérations de somme et d'application, de manière analogue au type Stream en Java 8 : ces opérations ne sont concrètement réalisées que quand on observe un élément du multi-ensemble.
3 Approche fonctionnelle
(Développer dans le paquet fonction pour cette partie.)
Avec l'approche fonctionnelle, on définit des modules, qui regroupent les opérations sur une structure élémentaire (sans opérations) de données.
Plusieurs interfaces sont fournies.
- paquet structuresAlgebriques.fonction : les structures algébriques, avec en particulier l'interface générique ModuleMonoideAdditif.
- paquet fonction :
- interface générique Sequence<S, E> définissant les accesseurs (sélecteurs et destructeurs) pour une séquence,
- interface générique FabriqueSequences<S, E> définissant les fabriques (associées aux constructeurs) de séquences,
- interface générique ModuleFoncteurSequence<S, E> définissant les méthodes associées au foncteur de séquence (taille, application, filtrage).
3.1 Diagramme des types
Voici le diagramme des types à obtenir.
Multi-ensembles paresseux
Module fonctionnel
3.2 Une interface pour les multi-ensembles
Définir l'interface générique MultiEnsembleParesseux<E> par héritage de Sequence (à instancier correctement). Y déclarer également les méthodes suivantes.
// Sélecteurs boolean estApplication(); boolean estSomme(); // Destructeurs Function<E, E> fonction(); MultiEnsembleParesseux<E> support(); MultiEnsembleParesseux<E> gauche(); MultiEnsembleParesseux<E> droit();
Un multi-ensemble est une application s'il est égal à une application d'une fonction à un multi-ensemble ; c'est une somme (union) s'il est égal à une somme. Aux sélecteurs sont associés des destructeurs, donnant d'une part la fonction et le multi-ensemble à laquelle la fonction est appliquée, d'autre part les deux termes de la somme.
3.3 Implémentation suivant le patron Composite
Définir quatre classes génériques d'implémentation de l'interface générique MultiEnsembleParesseux.
- MultiEnsembleVide : classe singleton représentant le multi-ensemble vide
- MultiEnsembleCons : classe représentant les multi-ensembles construits
- ApplicationMultiEnsemble : classe représentant l'application d'une fonction à un multi-ensemble
- MultiEnsembleSomme : classe représentant la somme de deux multi-ensembles
On donnera une implémentation par défaut de toutes les méthodes dans une classe abstraite générique MultiEnsembleComposite, permettant ainsi des factorisations :
- les sélecteurs renverront false,
- les destructeurs renverront une exception UnsupportedOperationException,
- la méthode taille renverra la valeur d'un attribut en lecture, à initialiser par le paramètre de l'unique constructeur.
Suivre les indications suivantes pour l'implémentation des quatre classes.
- Les définir par héritage de la classe abstraite MultiEnsembleComposite.
- Les sélecteurs et les destructeurs sont spécifiés ainsi dans chaque
classe d'implémentation.
estVide element, reste estApplication fonction, support estSomme gauche, droit Vide vrai non défini faux non défini faux non défini Cons faux défini faux non défini faux non défini Application ? ? vrai défini faux non défini Somme ? ? faux non défini vrai défini (? : deux cas possibles)
- Pour implémenter la classe singleton, on utilisera la méthode traditionnelle : classe finale (sans sous-classe), constructeur privé, constante statique publique de type MultiEnsembleParesseux<?>, méthode statique <E> MultiEnsembleParesseux<E> singleton() permettant de convertir la constante.
- Pour implémenter les méthodes element et reste dans les classes ApplicationMultiEnsemble et MultiEnsembleSomme, utiliser la récursivité.
3.4 Un module pour les multi-ensembles
Définir l'interface générique ModuleMultiEnsembles<E, Rep> par héritage de FabriqueSequences, ModuleFoncteurSequence et ModuleMonoideAdditif (à instancier correctement). Cette interface est paramétrée par un type E représentant le type des éléments et par un type Rep sous-type de Sequence<Rep, E>. L'étendre avec deux méthodes, boolean egal(Rep x, Rep y) et String representation(Rep x), analogues aux méthodes equals et toString.
3.5 Implémentation du module
Définir une classe générique ModuleMultiEnsemblesParesseux<E> implémentant ModuleMultiEnsembles<E, MultiEnsembleParesseux<E>>.
Suivre les indications suivantes.
- Implémenter les fabriques vide et cons ainsi que la méthode somme par appel des constructeurs des classes du composite.
- Définir récursivement la méthode application en distinguant les cas
suivants.
- Si le multi-ensemble est vide, renvoyer le vide.
- Si le multi-ensemble est une somme, renvoyer une somme.
- Si le multi-ensemble est une application, créer une nouvelle application de même support et de fonction la composition des deux fonctions.
- Si le multi-ensemble est construit (de type MultiEnsembleCons), créer une nouvelle application de manière paresseuse.
- Définir récursivement la méthode filtrage en distinguant les cas
suivants.
- Si le multi-ensemble est vide, renvoyer le vide.
- Si le multi-ensemble est une somme, renvoyer une somme filtrée.
- Si le multi-ensemble est une application, filtrer le support.
- Si le multi-ensemble est construit (de type MultiEnsembleCons), filtrer l'élément puis le reste.
- Implémenter la méthode egal en utilisant deux tables de hachage de
type Map<E, Integer> (initialisées par new HashMap<>()).
- Remplir la première table avec les éléments du premier multi-ensemble, en associant à chaque élément son nombre d'occurrences.
- Faire de même pour la seconde table.
- Comparer pour chaque élément le nombre d'occurrences dans les deux multi-ensembles.
- Implémenter la méthode representation par une boucle. Le multi-ensemble sera représenté sous la forme \(\{e_0, \ldots, e_n\}\).
- Définir un constructeur privé.
- Définir une fonction (statique) d'abstraction ainsi.
public static <E> ModuleMultiEnsembles<E, ?> abstraction(){ return new ModuleMultiEnsemblesParesseux<>(); }
Expliquer en commentaire à quoi elle sert.
3.6 Test
Tester l'implémentation du module en décommentant le corps de la classe TestMultiEnsemblesParesseux.
4 Approche objet
(Développer dans le paquet objet pour cette partie.)
Avec l'approche objet, on définit des données qui encapsulent les opérations.
Plusieurs interfaces sont fournies.
- paquet structuresAlgebriques.objet : les structures algébriques, avec en particulier l'interface générique MonoideAdditif.
- paquet objet : interface générique FoncteurSequence<E> définissant les méthodes associées au foncteur de séquence (taille, application, filtrage).
4.1 Diagramme des types
Voici le diagramme des types à obtenir.
Interface objet pour les multi-ensembles et adaptation du module
4.2 Interface pour les multi-ensembles
Définir l'interface générique MultiEnsemble par héritage de Sequence, FabriqueSequences, FoncteurSequence et de MonoideAdditif (à instancier correctement).
4.3 Implémentation des multi-ensembles par délégation
On implémente l'interface MultiEnsemble en déléguant les calculs à un multi-ensemble élémentaire (sans opérations) et à un module permettant d'opérer sur ce multi-ensemble. On réalise donc une adaptation du module à la nouvelle interface.
Précisément, définir une classe MultiEnsembleAvecModule<E, Rep> implémentant l'interface MultiEnsemble<E>. Le paramètre Rep représente le type des multi-ensembles élémentaires.
Suivre les indications suivantes.
- Le paramètre Rep doit être un sous-type de Sequence<Rep, E>>.
- La classe possède deux attributs, un module et un multi-ensemble.
private ModuleMultiEnsembles<E, Rep> module; private Rep multiEnsemble;
- Pour convertir de MultiEnsemble<E> vers Rep, utiliser la
méthode concretiser ainsi définie.
private Rep concretiser(MultiEnsemble<E> seq){ if(seq.estVide()) return module.vide(); return module.cons(seq.element(), concretiser(seq.reste())); }
- Inversement, pour fabriquer un MultiEnsemble<E> à partir d'un Rep,
utiliser la méthode fabriquer ainsi définie.
private MultiEnsemble<E> fabriquer(Rep multiEnsemble){ return new MultiEnsembleAvecModule<>(module, multiEnsemble); }
- Définir la méthode toString en utilisant la méthode representation.
4.4 Test
Dans la classe TestMultiEnsembles, décommenter le code et définir la fonction (statique) fabrique qui prend comme argument un module et renvoie une fabrique, en construisant un objet de type MultiEnsembleAvecModule. Veiller à réaliser la capture du joker (?) en quantifiant universellement la définition non seulement par un paramètre de type E pour les éléments mais aussi par un paramètre de type Rep pour la représentation des multi-ensembles élémentaires, abstraite par le joker.
5 Conclusion : intra-opérabilité contre inter-opérabilité
Lorsqu'on implémente les calculs dans le module ModuleMultiEnsemblesParesseux, on se restreint à des multi-ensembles du même type, MultiEnsembleParesseux, dont on connaît la structure. Il est impossible d'opérer avec des multi-ensembles d'un type différent : c'est l'intra-opérabilité. Avec une implémentation de l'interface MultiEnsemble, il est possible d'opérer avec toute autre implémentation de l'interface MultiEnsemble : c'est l'inter-opérabilité. L'inter-opérabilité a un coût, celui de la conversion d'un multi-ensemble quelconque en un multi-ensemble équivalent appartenant à l'implémentation : cf. la méthode concretiser.