Cours 2

Htmligure
Version & licenses
Creative Commons License

Les conteneurs.

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les conteneurs

Un type paramétré 'elt t est un conteneur si les valeurs de ce type servent à représenter des ensembles de valeurs de type 'elt. Le paramètre du type sert donc à préciser le types des valeurs contenues. Ce sont en général les structures de données. Les conteneurs les plus basiques en OCaml sont les listes, nous avons donc des listes d'entiers int list, de listes de caractères char list, des listes de listes de booléens bool list list, etc. Dans QuickCheck, le type Iter.t est un type conteneur. La librairie standard contient plusieurs autres types conteneurs, par exemple : les ensembles (module Set), les files (module Queue), les piles (module Stack), les dictionnaires (module Map), dont certains sont des types paramétrés.

Le principe d'un conteneur est de mettre des choses dedans, puis de pouvoir les en sortir plus tard. Cela permet de grouper des valeurs ensemble et de les manipuler de façon unifiée.

Les listes

Nous ferons un cours spécifique sur la manipulation des listes, nous n'introduisons ici que les bases. Une liste représente une séquence d'éléments de même type, manipulable par une seule de ses extrémités (que nous appelons par convention le début de la liste). Les fonctions de listes sont définies dans le module List de la librairie standard.

Une liste peut-être vide, on la note []. La liste vide est une valeur polymorphe : elle a le type elt list pour tout type elt, donc [] : 'elt list. On peut tester si une liste est vide en testant l'égalité avec [].

  1. let is_empty list = list = []
  2. let result = is_empty [2;3;5] (* false *)

Si une liste n'est pas vide, elle se décompose en un élément de tête (l'élément au début de la liste), et une queue (la liste de tous ses éléments, sauf la tête). On peut récupérer l'élément de tête avec la fonction List.hd : 'elt list -> 'elt, et la queue avec List.tl : 'elt list -> 'elt list.

  1. let head = List.hd [2;3;5] (* 2 *)
  2. let tail = List.tl [2;3;5] (* [3;5] *)

Ces deux fonctions provoquent une erreur à l'exécution si la liste est vide, donc :

  1. toujours tester avant que la liste est non-vide,
  2. on apprendra plus tard à ne jamais les utiliser.

Pour construire un liste, on peut le faire explicitement, en utilisant la notation [elt1;elt2;...;eltn]. Ou bien on peut le faire en ajoutant un élément à une liste déjà existante grâce à l'opérateur (::) : 'elt -> 'elt list -> 'elt list. L'insertion se fait en début de liste, il n'est pas possible d'insérer un élément en fin de liste.

  1. let first_list = [1;2;3;8;5]
  2. let second_list = 1 :: 2 :: 3 :: [8;5]
  3. let insert_42_in_list list = 42 :: list

QuickCheck.Iter

Le type Iter.t est utilisé par QuickCheck pour stocker l'ensemble des simplifications possibles d'une valeur. Par exemple dans le module de fraction, nous avons défini :

  1. let shrink_ratio (n,d) : Ratio.r Iter.t =
  2. let open QCheck.Iter in
  3. (if n <> 0 then return (Ratio.create (n/2) d) else empty)
  4. <+>
  5. (if d > 1 then return (Ratio.create n ((d + 1) / 2)) else empty)

Cette fonction dit que les simplifications de $\frac{n}{d}$ possibles sont $\frac{n/2}{d}$ (mais seulement si $n \neq 0$) et $\frac{n}{(d+1)/2}$ (mais seulement si $d > 1$). Autrement dit, si $\frac{n}{d}$ est un contre-exemple, on lui demande d'essayer si l'une des deux autres fractions ne serait pas aussi un contre-exemple.

Le résultat de l'appel à shrink_ratio r est donc une ensemble (peut-être vide) de fractions plus simples que r.

Nous avons utilisé plusieurs valeurs de Iter :

  • empty est l'ensemble vide,
  • return est une fonction créant un singleton avec l'argument qui lui est donné.
  • (<+>) est l'union de deux ensembles.

D'autres fonctions sont données. Par exemple, puisque les listes sont des conteneurs, il est naturel de penser qu'on peut convertir une liste en un conteneur de type Iter.t. C'est ce que fait la fonction of_list. La plupart des conteneurs donnent des fonctions de conversion depuis ou vers les listes.

Voici un exemple alambiqué de création d'un ensemble d'entiers.

  1. (* A function to display a container. *)
  2. let print_all to_string iterable =
  3. iterable (fun arg -> Printf.printf "%s\n%!" (to_string arg))

  4. let int_container =
  5. let open QCheck.Iter in
  6. of_list [2;3;4]
  7. <+> return 6
  8. <+> empty
  9. <+> of_list [8;9]
  10. <+> return 0

On vérifie le contenu de l'ensemble :

  1. # let () = print_all QCheck.Print.int int_container;;
  2. 2
  3. 3
  4. 4
  5. 6
  6. 8
  7. 9
  8. 0

Puisque Iter.t est un type paramétré, on peut faire des conteneurs contenant n'importe quel type de valeurs (du moment que toutes les valeurs du conteneur sont du même type). Nous pouvons faire des conteneurs d'entiers, des conteneurs de flottants, etc. Une conséquence est que si on dispose d'un conteneur d'entiers, et d'une fonction des entiers vers les flottants, en appliquant la fonction à tous les éléments du conteneur d'entier nous devrions pouvoir obtenir un conteneur de flottant. Effectivement, c'est ce que fait la fonction map : 'elt t -> ('elt -> 'image) -> 'image t. À nouveau, la plupart des conteneurs possède une fonction map (en particulier les listes). Convertissons notre conteneur d'entier en conteneur de flottant :

  1. (* [float_of_int] is the OCaml function to convert ints into floats *)
  2. let float_container =
  3. QCheck.Iter.map float_of_int int_container

Puis affichons le contenu :

  1. # let () = print_all QCheck.Print.float float_container;;
  2. 2.
  3. 3.
  4. 4.
  5. 6.
  6. 8.
  7. 9.
  8. 0.

On peut aussi simplement faire une manipulation sur les entiers du premier conteneur :

  1. # let another_int_container =
  2. QCheck.Iter.map (fun n -> 2 * n + 1) int_container;;
  3. val another_int_container : int QCheck.Iter.t = <fun>
  4. # let () = print_all QCheck.Print.int another_int_container;;
  5. 5
  6. 7
  7. 9
  8. 13
  9. 17
  10. 19
  11. 1

Nous avons dit plus tôt que les conteneurs contiennent des valeurs que l'on peut ensuite récupérer. Iter.t est un type de conteneur très simple, et il n'y a pas de fonctions de récupération dans le module Iter. La façon de récupérer les valeurs du conteneur est cachée dans la définition du type Iter :

  1. type 'elt t = ('elt -> unit) -> unit

La seule façon d'utiliser les éléments de container : elt t est d'appliquer container (qui est en fait une fonction) à une fonction utilisant ses éléments en argument. C'est ce que nous avons fait plus haut pour la fonction print_all. Cela peut sembler étrange, mais ce n'est pas étonnant lorsqu'on sait qu'en programmation fonctionnelle, tout peut être représenté uniquement par des fonctions.

Iter ne propose pas de fonction plus élaborée pour accéder au contenu d'un conteneur. Pourquoi ? car ce type sert à l'utilisateur de QuickCheck à mettre des valeurs à disposition de l'algorithme de test. L'utilisateur n'est pas censé récupérer les valeurs de ce conteneur.

Retour au sommaire.