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 ?
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 :
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).
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).
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 :
ainsi que des variantes, pour avoir des valeurs plus contraintes, par exemple :
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.
Par exemple :
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 :
Un autre combinateur intéressant permet de choisir un générateur aléatoire parmi plusieurs :
QuickCheck.Gen contient plein d'autres combinateurs intéressants, qui devraient répondre à tous nos besoins.
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 :
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 :
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 :
On peut détailler les arguments :
Nous pouvons maintenant comprendre l'exemple du générateur de fractions. Il nous faut :
On assemble pour obtenir le générateur d'instance :