Retour au sommaire.
QuickCheck.
Pour étudier les types paramétrés, nous allons utiliser l'exemple de QuickCheck. QuickCheck est une librairie qui permet de réaliser des tests automatisés de fonctions OCaml. Le principe est d'écrire une fonction prenant une valeur et un booléen, QuickCheck génére aléatoirement plein de valeurs différentes et teste la fonction avec, puis reporte s'il a trouvé une valeur pour laquelle la fonction retourne false.
Nous utiliserons QuickCheck tout au long du cours pour tester nos programmes, donc il est utile d'apprendre à l'utiliser immédiatement. Par ailleurs, QuickCheck repose sur plusieurs types paramétrés ayant différents rôles, ce qui permet bien d'illustrer les usages possibles des types paramétrés.
QuickCheck est un outil très complet, nous n'étudierons pas toutes ses fonctionnalités. À noter que QuickCheck est une adaptation d'une librairie Haskell du même nom, et qu'il en existe maintenant des versions dans de nombreux langages, par exemple en Java.
Un petit exemple.
Commençons par essayer sur un petit exemple, pour voir comment cela fonctionne.
module Ratio =
struct
type t = (int * int)
let zero = (0,1)
let one = (1,1)
let rec gcd n p =
if p = 1 then n
else gcd p (n mod p)
let create n d =
let p = gcd n d in
let (n', d') = ( n/p, d/p ) in
if d' < 0 then ( -n', -d')
else (n', d')
let ( +/ ) (n1,d1) (n2,d2) = create (n1+n2) (d1+d2)
let ( */ ) (n1,d1) (n2,d2) = create (n1*n2) (d1*d2)
let equals (n1,d1) (n2,d2) = n1 = n2 && d1 = d2
let pp formatter (n,d) =
Format.fprintf formatter "@[%d/%d@]" n d
let to_string (n,d) =
Printf.sprintf "%d/%d" n d
end
Ce module permet de représenter les fractions (format : (numérateur, dénominateur)) et de faire des calculs avec. Malheureusement, il n'est pas correct. Nous allons utiliser QuickCheck pour trouver les erreurs et les corriger.
D'abord, nous allons charger QuickCheck dans l'interpréteur :
#use "topfind";;
#require "qcheck";;
#install_printer Ratio.pp;;
Ensuite, nous allons écrire quelques propriétés qui doivent être vérifiés par les fonctions que nous avons définies. En voici un petit échantillon, qui utilise les propriétés de bases d'une algèbre :
let gcd_is_common_divisor (n,p) =
let d = Ratio.gcd n p in
n mod d = 0 && p mod d = 0
let gcd_with_a_multiple_is_itself (n,p) =
Ratio.gcd n (p * n) = n
let zero_is_addition_neutral ratio =
Ratio.(equals (ratio +/ zero) ratio)
&& Ratio.(equals (zero +/ ratio) ratio)
let addition_is_commutative (ratio1,ratio2) =
Ratio.(equals (ratio1 +/ ratio2) (ratio2 +/ ratio1))
let addition_is_associative (ratio1, ratio2, ratio3) =
Ratio.(equals
(ratio1 +/ (ratio2 +/ ratio3))
((ratio1 +/ ratio2) +/ ratio3)
)
let one_is_multiplication_neutral ratio =
Ratio.(equals (ratio */ one) ratio)
&& Ratio.(equals (one */ ratio) ratio)
let multiplication_is_distributive (ratio1, ratio2, ratio3) =
Ratio.(equals
((ratio1 +/ ratio2) */ ratio3)
(ratio1 */ ratio3 +/ ratio2 */ ratio3)
)
QuickCheck génère des tests aléatoirement, nous devons lui apprendre à générer une fraction aléatoirement.
let shrink_ratio (n,d) =
let open QCheck.Iter in
(if n <> 0 then return (Ratio.create (n/2) d) else empty)
<+>
(if d > 1 then return (Ratio.create n ((d + 1) / 2)) else empty)
let ratio_gen =
let open QCheck.Gen in
pair (-1000 -- 1000) (1 -- 1000)
|> map (fun (n,d) -> Ratio.create n d)
let ratio_arbitrary : Ratio.t QCheck.arbitrary =
QCheck.make
~print:Ratio.to_string
~shrink:ratio_shrink
ratio_gen
QCheck.arbitrary est un type paramétré pour les générateurs aléatoires de valeurs utilisés par QuickCheck. ratio_arbitrary a donc le type d'un générateur de fractions, ce que l'on voulait. Son code est pour l'instant magique, on l'expliquera plus tard.
Il nous faut aussi un générateur de paires d'entiers pour tester la fonction de pgcd. Le voilà :
let int_pair_arbitrary : (int * int) QCheck.arbitrary =
QCheck.(pair small_int small_int)
À nouveau, le type paramétré de int_pair_arbitrary correspond à ce que nous désirons.
Commençons donc par tester le calcul de pgcd.
# let test_gcd =
QCheck.Test.make
~name:"Test common divisor"
int_pair_arbitrary
gcd_is_common_divisor
|> QCheck.Test.check_exn;;
Exception:
QCheck.Test.Test_fail ("Test common divisor",
["(0, 0) (after 16 shrink steps)"]).
Il trouve donc un erreur (Test_fail), pour la paire (0,0). Il indique aussi avoir fait 16 étapes de réductions : il essaie toujours de trouver un plus petit contre-exemple possible, en utilisant des règles de simplifications sur les contre-exemples. On vérifie maintenant l'erreur pour essayer de la comprendre :
# let res = Ratio.gcd 0 0;;
Exception: Division_by_zero.
Une division par zéro ! on corrige donc l'algorithme de pgcd, en remplaçant le test p = 1 par p = 0, qui est le bon cas de base.
Reprenons les tests :
# let test_common_divisor =
QCheck.Test.make
~name:"test common divisor"
int_pair_arbitrary
gcd_is_common_denominator
|> QCheck.Test.check_exn
# let test_divisor_of_multiples =
QCheck.Test.make
~name:"test divisor of multiples"
int_pair_arbitrary
gcd_with_a_multiple_is_itself
|> QCheck.Test.check_exn
# let test_zero_neutral =
QCheck.Test.make
~name:"test zero neutral"
ratio_arbitrary
zero_is_addition_neutral
|> QCheck.Test.check_exn
Exception:
QCheck.Test.Test_fail ("test zero neutral",
["-1/1 (after 12 shrink steps)"]).
Le calcul du pgcd semble être corrigé. Par contre l'addition n'est pas bonne, puisque zéro ne semble pas être neutre. Faisons le calcul :
# Ratio.(create (-1) 1 +/ zero);;
- : Ratio.t = -1/2
C'est effectivement clairement faux. La raison est simple, le code de l'addition est complètement faux, n'importe quel collégien s'en rendrait compte... On le corrige :
let ( +/ ) (n1,d1) (n2,d2) = ( n1*d2 + n2*d1, d1*d2)
Puis on continue de tester :
let test_addition_is_commutative =
QCheck.Test.make
~name:"test addition commutative"
QCheck.(pair ratio_arbitrary ratio_arbitrary)
addition_is_commutative
|> QCheck.Test.check_exn
let test_addition_is_associative =
QCheck.Test.make
~name:"test addition associative"
QCheck.(triple ratio_arbitrary ratio_arbitrary ratio_arbitrary)
addition_is_associative
|> QCheck.Test.check_exn
let test_one_neutral =
QCheck.Test.make
~name:"Test one neutral"
ratio_arbitrary
one_is_multiplication_neutral
|> QCheck.Test.check_exn
Les trois tests suivants passent sans problème. Pour tester la commutativité de l'addition, il faut deux fractions. Le deuxième argument de QCheck.Test.make précise donc qu'il faut tirer des paires de fractions. De même pour l'associativité de l'addition, on tire des triplets de fractions.
Le dernier test pose problème :
# let test_multiplication_is_distributive =
QCheck.Test.make
~name:"test multiplication distributive"
QCheck.(triple ratio_arbitrary ratio_arbitrary ratio_arbitrary)
multiplication_is_distributive
|> QCheck.Test.check_exn;;
Exception:
QCheck.Test.Test_fail ("test multiplication distributive",
["(-1/1, -1/1, -1/2) (after 43 shrink steps)"]).
Regardons ce qui se passe :
# let _ =
let ratio1 = Ratio.create (-1) 1 in
let ratio2 = Ratio.create (-1) 1 in
let ratio3 = Ratio.create (-1) 2 in
( Ratio.( (ratio1 +/ ratio2) */ ratio3 ),
Ratio.( ratio1 */ ratio3 +/ ratio2 */ ratio3)
);;
- : Ratio.t * Ratio.t = (1/1, 4/4)
Les deux fractions sont égales, mais la deuxième n'est pas sous forme canonique. Normalement tous les fractions doivent être mises sous forme canonique à leur création. Le problème vient de l'addition, qui n'a pas simplifié le résultat du second calcul. Il faut passer par create pour écrire l'addition.
On obtient alors le module suivant, qui passe tous les tests, et dont on peut humblement espérer qu'il soit enfin sans erreur.
module Ratio =
struct
type t = (int * int)
let zero = (0,1)
let one = (1,1)
let rec gcd n p =
if p = 0 then n
else gcd p (n mod p)
let create n d =
let p = gcd n d in
let (n', d') = (n/p, d/p) in
if d' < 0 then (-n', -d')
else (n', d')
let ( +/ ) (n1,d1) (n2,d2) = create (n1*d2 + n2*d1) (d1*d2)
let ( */ ) (n1,d1) (n2,d2) = create (n1*n2) (d1*d2)
let equals (n1,d1) (n2,d2) = n1 = n2 && d1 = d2
let pp formatter (n,d) =
Format.fprintf formatter "@[%d/%d@]" n d
let to_string (n,d) =
Printf.sprintf "%d/%d" n d
end
Récapitulatif.
QuickCheck est un outil de génération automatique d'instances pour réaliser des tests unitaires. Pour réaliser un test il faut :
écrire un fonction à un argument et retournant un booléen,
écrire un générateur, de type arg QuickCheck.arbitrary,
utiliser la fonction QuickCheck.Test.make, avec en argument le générateur et la fonction à tester, ce qui crée le test,
appeler la fonction QuickCheck.Test.check_exn sur le test, ce qui provoque son exécution.
QuickCheck génère des tests, les fait passer, simplifie automatiquement les contre-exemples et reporte le résultat. Nous l'utiliserons dorénavant et pour tout le semestre pour vérifier nos programmes.
QuickCheck utilise plusieurs types paramétrés :
Gen.t le type des générateurs aléatoires,
Print.t le type des conversions en chaînes de caractères,
Iter.t qui permet de décrire les réductions possibles d'une instance,
Shrink.t qui représente les fonctions de réductions,
arbitrary le type des générateurs d'instances.
Test.cell qui représente un test unitaire.
Tous ces types ne s'utilisent pas de la même façon, mais rentrent dans trois grandes classes de types paramétrés possibles. C'est ce que nous allons détailler.
Retour au sommaire.