UP | HOME

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.

medias/exam_multiset_function.png

Multi-ensembles paresseux

medias/exam_multisetModule_function.png

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.

medias/exam_multiset_object.png

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.

Last Updated 2015-12-03T09:58+0100. Comments or questions: Send a mail.