Cours 2

Htmligure
Version & licenses
Creative Commons License

Les producteurs

Guyslain
Thursday, 21 July 2016

Retour au sommaire.

Les producteurs.

Une autre catégorie de types paramétrés est formée par les types encodant des fonctions de productions de valeurs, des fonctions capables de générer de nouvelles valeurs d'un type précis. Le type 'value t est alors un producteur de valeur du type 'value. Le paramètre du type sert donc à encoder quel est le type des valeurs générées.

On ne peut pas générer des valeurs en partant de rien. Si par exemple nous voulions avoir une fonction de production n'utilisant que le producteur d'entier, que se passerait-il ?

  1. let gen producer = ???
  2. val gen : int producer -> int

  3. let test =
  4. let a : int = gen int_producer in
  5. let b : int = gen int_producer in
  6. a = b (* always true *)

Nous avons vu au premier cours qu'une expression a une valeur, donc gen int_producer possède une et une seule valeur. Autrement dit, gen int_producer retourne toujours la même chose, ce qui n'est pas intéressant : chaque producteur représenterait une et une seule valeur. La seule solution est d'avoir un argument qui apporte des informations au producteur, et le producteur utilise ces informations pour produire des valeurs. La fonction de génération a alors un type plus proche de celui-ci :

  1. val gen : 'a producer -> info -> ('a, info)

On peut alors appeler plusieurs fois le générateur avec plusieurs arguments d'informations différents (en réutilisant la sortie de la génération précédente).

  1. let triple_of_ints int_producer info0 =
  2. let (int1, info1) = gen int_producer info0 in
  3. let (int2, info2) = gen int_producer info1 in
  4. let (int3, info3) = gen int_producer info2 in
  5. ((int1, int2, int3), info3)

On a ainsi généré un triplet d'entier, pas nécessairement identiques, et on retourne aussi l'information finale pour pouvoir continuer à générer d'autres entiers au besoin. Les producteurs sont donc accompagnés par un type servant de sources d'information (ce type peut cependant ne pas apparaître dans l'interface).

QCheck.Gen

Un premier exemple de producteur est le type des générateurs aléatoires de QuickCheck : value QCheck.Gen.t est un générateur aléatoire de valeurs de types 'value. Le type des informations utilisées par ces générateurs est Random.State.t, provenant du module Random de la librairie standard.

QCheck.gen propose plusieurs générateurs pré-écrits, en particulier un générateur pour chacun des types de bases :

  1. val bool : bool Gen.t
  2. val int : int Gen.t
  3. val float : float Gen.t
  4. val char : char Gen.t

ainsi que des variantes, pour avoir des valeurs plus contraintes, par exemple :

  1. val small_int : int Gen.t (* integers, but without too many digits *)
  2. val neg_int : int Gen.t (* negative integers *)
  3. val pfloat : float Gen.t (* positive floats *)
  4. val nfloat : float Gen.t (* negative floats *)
  5. val printable : char Gen.t (* printable characters *)
  6. val (--) : int -> int -> int Gen.t
  7. (* [a -- b] generates integers between [a] and [b] inclusively *)

Si nous pouvons générer un entier et si nous pouvons générer un flottant, nous devrions pouvoir générer la paire d'un entier et d'un flottant. De même, si nous pouvons générer des entiers, nous devons pouvoir générer des triplets d'entiers, comme nous l'avons fait plus haut. QCheck.Gen sait tout cela et propose donc des fonctions qui permettent de combiner des générateurs aléatoires pour en faire d'autres capables de créer des valeurs plus complexes.

  1. val pair : 'left Gen.t -> 'right Gen.t -> ('left * 'right) Gen.t

  2. val triple :
  3. 'left Gen.t
  4. -> 'middle Gen.t
  5. -> 'right Gen.t
  6. -> ('left * 'middle* 'right) Gen.t

  7. val list : 'elt Gen.t -> 'elt list Gen.t

  8. val opt : 'elt Gen.t -> 'elt option Gen.t

Par exemple :

  1. (* un générateur de listes de paires d'entiers *)
  2. # let int_pair_list = QCheck.Gen.(list (pair int int))
  3. val int_pair_list : (int * int) list QCheck.Gen.t

  4. (* un générateur de paires de listes de caractères *)
  5. # let char_list_pair = QCheck.Gen.(pair (list char) (list char))
  6. val char_list_pair = (char list * char list) QCheck.Gen.t

Il est donc étonnamment simple de créer des générateurs aléatoires, par combinaison de générateurs aléatoires pour les types élémentaires. Les fonctions créant les combinaisons sont appelés combinateurs, et nous verrons plein de librairies utilisant des combinateurs.

QCheck.Gen contient d'autres combinateurs. Par exemple, si on sait générer des flottants, et qu'on a une fonction des flottants vers les chaînes de caractères, on doit savoir générer des chaînes de caractères : on génère un flottant, on applique la fonction, et hop, on a une chaine de caractères. Nous avons eu le même raisonnement avec les conteneurs, sans surprise nous retrouvons donc une autre fonction portant le nom map :

  1. val map : ('elt -> 'im) -> 'elt QCheck.Gen.t -> 'im QCheck.Gen.t

  2. let non_negative_int : int QCheck.Gen.t =
  3. map (fun i -> abs i) int (* [abs : int -> int] is the absolute value *)

  4. let ratio : Ratio.t QCheck.Gen.t =
  5. pair int int
  6. |> map (fun n d -> Ratio.create n d)

Un autre combinateur intéressant permet de choisir un générateur aléatoire parmi plusieurs :

  1. let int_with_leading_9 : int QCheck.Gen.t =
  2. QCheck.Gen.(oneof
  3. [ return 9; (* this always generates 9 *)
  4. 90 -- 99;
  5. 900 -- 999;
  6. 9000 -- 9999
  7. ])

  8. (* To get a uniform distribution, use [frequency]; *)
  9. let uniform_int_with_leading_9 : int QCheck.Gen.t =
  10. QCheck.Gen.(frequency
  11. [ (1, return 9);
  12. (10, 90 -- 99);
  13. (100, 900 -- 999);
  14. (1000, 9000 -- 9999)
  15. ])

QuickCheck.Gen contient plein d'autres combinateurs intéressants, qui devraient répondre à tous nos besoins.

QuickCheck.arbitrary

QuickCheck contient aussi un type paramétré arbitrary qui code un producteur d'instances à tester. On pourrait penser qu'un générateur aléatoire suffit, mais il faut aussi :

  • pourvoir afficher l'instance à tester, donc la convertir en chaîne de caractères,
  • attribuer un critère de taille à chaque instance, pour pouvoir retourner un plus petit contre-exemple,
  • définir une façon de simplifier les instances, pour pouvoir trouver des contre-exemples simples.

Seul le générateur aléatoire est obligatoire. En l'absence de critère de simplification, de taille ou de conversion en chaîne de caractères, les fonctionnalités liées ne seront pas utilisées.

Une valeur de type value QCheck.arbitrary est donc un générateur d'instances de tests aléatoires. On peut créer un tel générateur avec la fonction suivante :

  1. val make : (* simplified *)
  2. ?print:QCheck.Print.t
  3. -> ?small:('value -> int)
  4. -> ?shrink:('value -> 'value QCheck.Iter.t)
  5. -> 'value QCheck.Gen.t
  6. -> 'value QCheck.arbitrary

D'abord, que veut-dire le point d'interrogation devant un label, comme ?print ou ?shrink ? Simplement que l'argument est optionnel, on peut appeler la fonction sans cet argument. Par exemple, il est valide de définir :

  1. let int_arbitrary = (* no optional arguments, that's ok *)
  2. make QCheck.Gen.int

  3. let better_int_arbitrary = (* shrink is missing, that's ok *)
  4. make
  5. ~print:QCheck.Print.int
  6. ~small:(fun i -> abs i)
  7. QCheck.Gen.int

  8. let best_int_arbitrary = (* everything is defined, cool *)
  9. make
  10. ~print:QCheck.Print.int
  11. ~small:(fun i -> abs i)
  12. ~shrink:QCheck.Shrink.int
  13. QCheck.Gen.int

On peut détailler les arguments :

  • print passe la fonction de conversion en chaîne de caractères. Nous apprendrons à construire ces fonctions dans la prochaine section.
  • small est le critère de taille : c'est une fonction attribuant un entier à chaque instance. Plus l'entier est petit, plus l'instance est considérée comme simple.
  • shrink est la fonction de réduction : elle associe à chaque instance un conteneur d'instances plus petites. Le conteneur est sans surprise de type 'value Iter.t.
  • le dernier argument, non-nommé et obligatoire, est le générateur aléatoire.

Nous pouvons maintenant comprendre l'exemple du générateur de fractions. Il nous faut :

  • un générateur aléatoire. Notons qu'on veut un dénominateur strictement positif.
  1. let ratio_gen : Ratio.t QCheck.Gen.t =
  2. let open QCheck.Gen in
  3. pair (-1000 -- 1000) (1 -- 1000)
  4. |> map (fun (n,d) -> Ratio.create n d)
  • une fonction de réduction. On réduit le numérateur vers 0 , ou le dénominateur vers 1 , si possible.
  1. let shrink_ratio (n,d) =
  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)

  • une fonction de conversion en chaîne de caractères. C'est Ratio.to_string.
  • on ne va pas mettre de critère de taille (on pourrait choisir le dénominateur par exemple).

On assemble pour obtenir le générateur d'instance :

  1. let ratio_arbitrary : Ratio.t QCheck.arbitrary =
  2. QCheck.make
  3. ~print:Ratio.to_string
  4. (* ~small:(fun (n,d) -> d) *)
  5. ~shrink:ratio_shrink
  6. ratio_gen

Retour au sommaire.