Cours 6

Htmligure
Version & licenses
Creative Commons License

Interfaces usuelles.

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Quelques interfaces fréquemment utilisés.

Les types ordonnés.

Comme en Java, l'une des interfaces les plus utiles permet de préciser que des valeurs sont comparables (sauf que précisement Comparable n'est pas une interface, une bizarrerie de Java). En Java on compare des objets, en OCaml on compare en général des valeurs du même type. On peut donc définir une interface pour les types comparables :

  1. module type OrderedType = sig
  2. type t
  3. val compare : t -> t -> int
  4. end

Pour avoir un type comparable, il faut un type, appelé t, et une fonction de comparaision appelé compare. La comparaison suit les conventions habituelles, la valeur retournée doit être un entier, en cohérence à ce que produirait une soustraction entre les deux arguments : si le premier argument est le plus grand le résultat doit être positif, s'il est plus petit le résultat doit être négatif, et le résultat est nul si les éléments sont incomparables.

Dans la librairie standard, cet interface est définie sous le nom Set.OrderedType. Le module String contient une définition de t et de compare compatibles :

  1. (* from string.mli *)
  2. type t = string
  3. ...
  4. val compare : t -> t -> int
  5. ...

On peut donc écrire :

  1. module OrderedString = (String : Set.OrderedType)

ce qui a pour effet de définir un nouveau module dont la signature est exactement celle donnée par Set.OrderedType (en particulier, le type OrderedString.t est alors abstrait).

Il n'y a par contre pas de module contenant un type appelé t synomyme d'entier, et une fonction comparecorrespondante, mais il est possible de le créer :

  1. module Int = struct
  2. type t = int
  3. let compare a b = a - b
  4. end

Les module Int et String implémentent tous deux l'interface Set.OrderedType.

L'interface hachable.

De même, la librairie standard fournit l'interface des types hachables, disposant d'une fonction de hachage :

  1. module type Hashtbl.HashedType = sig
  2. type t
  3. val equal : t -> t -> bool
  4. val hash : t -> int
  5. end

Nous voyons que le type doit être fourni avec une fonction de hachage hash et un test d'égalité.

Les dictionnaires.

La librairie standard définit quelques interfaces supplémentaires, comme celle des dictionnaires Map.S.

C'est une interface plutôt longue contenant de nombreuses fonctions. En voici un extrait :

  1. module type Map.S = sig
  2. type key
  3. type 'assoc t
  4. val empty : 'assoc t
  5. val is_empty : 'assoc t -> bool
  6. val add : key -> 'assoc -> 'assoc t -> 'assoc t
  7. val mem : key -> 'assoc t -> bool
  8. val find : key -> 'assoc t -> 'assoc
  9. val remove : key -> 'assoc t -> 'assoc t
  10. val compare : 'assoc t -> 'assoc t -> int
  11. ...
  12. end

La longueur de cette interface suggère qu'il n'est pas ici question de spécifier ce que doit implémenter un module dans le but de pouvoir l'utiliser dans un contexte précis. Dans un tel cas de figure, on aimerait plutôt préciser un nombre minimum d'opérations nécessaire. De fait, cette interface est l'interface d'un module qui peut être généré dans un certain contexte que nous verrons bientôt.

Une autre chose à noter est la présence d'une fonction compare dans Map.S. Cela veut-il dire que Map.S contient Set.OrderedType ? En fait non, car dans Set.OrderedType, le type t n'est pas paramétré. Les deux interfaces sont donc incompatibles; les deux types nommés t ne peuvent pas être les mêmes car ils n'ont pas le même nombre de paramètres.

Les monoïdes.

À toute structure algébrique on peut associer une interface. À titre d'exemple, et comme ils sont particulièrement utiles au programmeur, voici l'interface d'un monoïde :

  1. module type Monoid = sig
  2. type t
  3. val zero : t
  4. val append : t -> t -> t
  5. end

Il ne faut pas oublier qu'en mathématiques, les structures algébriques sont surtout définies par des lois : par exemple l'addition (qu'on a appelé append ici) est associative dans un monoïde, ou le zéro est neutre pour l'addition. Il n'est pas possible en OCaml de préciser ou de forcer le respect de ces lois, c'est au programmeur de s'assurer du respect des lois.

Des exemples de monoïdes sont : les entiers avec l'addition, les listes ou les chaînes de caractères avec la concaténation, des fonctions d'un ensemble dans lui-même avec la composition.

Les foncteurs et monades.

Nous avons vu de nombreux types paramétrés, et la plupart avait une fonction map, qui avait systématiquement le même type. Cela nous invite à définir l'interface suivante :

  1. module type Functor = sig
  2. type 'a t
  3. val map : ('a -> 'b) -> 'a t -> 'b t
  4. end

De même, nous avons souvent croisé des fonctions bind (ou sous la forme d'opérateur (>>=)). L'interface correspondante est :

  1. module type Monad = sig
  2. type 'a t
  3. val return : 'value -> 'value t
  4. val (>>=) : 'value t -> ('value -> 'result t) -> 'result t
  5. end

Les foncteurs et les monades ont aussi des lois :

  1. (* using == as the equivalence symbol *)

  2. (* Map *)
  3. map id == id (* id = fun x -> x *)
  4. map (fun x -> x |> g |> f) == fun x -> x |> map g |> map f

  5. (* Monad *)
  6. return a >>= f == f a
  7. m >>= return == m
  8. m >>= (fun x -> g x >>= f) == (m >>= g) >>= f

Les traversables.

Une autre interface usuelle correspond aux itérables de Java. Sur une collection de données nous voulons pouvoir réaliser un calcul dépendant de toutes les valeurs dans la collection. Une interface possible est de demander une fonction fold pour la collection.

  1. module type Foldable = sig
  2. type 'content t
  3. val fold : ('state -> 'content -> 'state) -> 'state -> 'content t -> 'state
  4. end

Il existe encore plusieurs autres interfaces classiques, mais notre objectif n'est pas d'être exhaustif, ni même d'apprendre à utiliser toutes ces interfaces dans ce cours. Les librairies d'OCaml n'insistent pas particulièrement sur ces interfaces : beaucoup de librairies implémentent plusieurs de ces interfaces sans spécifiquement y faire référence. Par contre, être capable de reconnaître et utiliser correctement ces interfaces permet de comprendre et utiliser correctement de nouvelles librairies utilisant les mêmes abstractions.

Dans le langage Haskell, ces interfaces sont bien plus importantes car elles sont utilisées explicitement à travers un système de classes propre à ce langage. Une quantité très importante de fonctions en Haskell dérivent des principales interfaces du langage et sont donc communes à la plupart des librairies.

Retour au sommaire.