QuickCheck es una herramienta que el lenguaje de programación Haskell provee para poder probar las propiedades que deberían de cumplir las funciones, es decir, cada función tiene propiedades deseables lo que se logra con QuickCheck es ver si se cumplen total o parcialmente estas propiedades. Una ventaja notoria es que la propiedad es probada con una gran cantidad de casos generados aleatoriamente. Por ejemplo si tenemos una función suma:
suma x y = x + y
para ver si cumple la propiedad conmutativa de la suma de números enteros, para cualquier entero:
prop_suma_conmutativa :: Int ->Int -> Bool prop_suma_conmutativa x y = suma x y == suma y x
la función prop_suma_conmutativa al ser pasada como parámetro en quickCheck será verificada con varios casos aleatorios de enteros e indicará si eventualmente la función suma cumple o no la propiedad de la suma de números enteros.
Detalles Teóricos importantes
import Test.QuickCheck insertOrdered :: (Ord a) => a -> [a] -> [a] insertOrdered a [] = [a] insertOrdered a (x:xs)| a <= x = a:[x] ++ xs | a > x = x: insertOrdered a xs prop_insertOrdered a (xs) = ordered(insertOrdered a xs) -- main = quickCheck prop_insertOrdered >:l Main Main> quickCheck prop_insertOrdered ERROR - Unresolved overloading
Notar en este ejemplo:
Combinador condicional: Podemos mejorar un poco el ejemplo anterior añadiendo una condición, a nuestra propiedad.
prop_InsertOrdered :: Integer -> [Integer] -> Property prop_InsertOrdered x xs = ordered xs ==> ordered (insertOrdered x xs [])
La estructura para utilizar el combinador condicional:
<condition> ==> <property>
Combinador classify: El combinador classify no cambia el significado de la propiedad pero permite clasificar algunos casos, se podría decir que es una forma de etiquetarlos. En el ejemplo anterior veamos la cantidad de listas producidas aleatoriamente de tamaño menor a uno y listas mayores de dos elementos.
prop_InsertOrdered :: Int -> [Int] -> Property prop_InsertOrdered x xs = ordered xs ==> classify (length xs <= 1) "lista de tam < 1" $ classify (length xs > 2) "lista de tam > 2" $ ordered (insertOrdered x xs) *Main> main OK, passed 100 tests. 79% lista de tam < 1. 4 % lista de tam > 2. *Main>
La estructura para utilizar el combinador classify:
classify <condition><string>$ <property>
Como vemos solo un 4% de las listas generadas aleatoriamente son de tamaño mayor a dos.
Combinador collect: El combinador collect cosecha todos los valores que son pasados e imprime un histograma de esos, aquí podemos darnos cuenta que solo un porcentaje menor de casos de prueba son probados porque (en el caso específico) las listas de 1 o 0 elementos son ordenados pero los mayores a 2 elementos no lo son, este es un riesgo al utilizar leyes condicionales.
prop_InsertOrdered :: Int -> [Int] -> Property prop_InsertOrdered x xs = ordered xs ==> collect (length xs >1)$ collect (length xs)$ ordered (insertOrdered x xs) *Main> main OK, passed 100 tests. 49% False, 0. 34% False, 1. 13% True, 2. 3% True, 3. 1% True, 4.
La estructura para utilizar el combinador collect:
collect <expression> $ <property>
Combinador forAll: El combinador forAll nos indica que para toda lista ordenada se podrá probar la propiedad.
prop_InsertOrdered :: Integer -> Property prop_InsertOrdered x = forAll orderedList $ \ xs -> collect (length xs>2)$ collect (length xs)$ ordered (insertOrdered x xs) -- el generador de listas ordenadas, -- solo es una funcion que crea listas ordenadas orderedList :: (Ord a, Arbitrary a) => Gen [a] orderedList = frequency [(1,return []), (4,do xs <- orderedList n <- arbitrary return ((case xs of [] -> n x:_ -> n ‘min‘ x) :xs))] Main> main OK, passed 100 tests. 18% False, 1. 16% False, 0. 15% False, 2. 10% True, 8. 10% True, 3. 8% True, 4. 5% True, 5. 4% True, 9. 4% True, 13. 3% True, 6. 2% True, 7. 2% True, 11. 2% True, 10. 1% True, 16.
La estructura para utilizar el combinador forAll:
forAll <generator>$\<pattern> -> <property>
Solo observando: Veamos la distribución de los casos de prueba...
prop_InsertOrdered :: Integer -> [Integer] -> Property prop_InsertOrdered x xs = ordered xs ==> (length xs <= 1) ‘trivial‘ ordered (insertOrdered x xs) *Main> main OK, passed 100 tests (86% trivial).
La estructura para ver la distribución es:
<condition> ‘trivial‘ <property>
Indicamos que las listas generadas por la funci´on orderedList son triviales en aquellos casos que su tamaño sea menor o igual a uno La respuesta nos dice que el 86% de los casos son triviales.
Definiendo Generadores Propios Para definir generadores propios para nuestros tipos de datos utilizamos la Clase Arbitrary.
data Insectos = Polillas | Mariposas | OtrosBichos instance Arbitrary Insectos where arbitrary = oneof [ return Mariposas, return Polillas , return OtrosBichos]
oneof: Podemos utilizar el combinador oneof escoge de una lista de generadores alternativos con una distribución uniforme, los constructores. frequency: Veamos la siguiente estructura, un árbol binario con dos funciones tam que nos devuelve el tamaño del árbol y arbolALista que nos devuelve los elementos de un árbol en una lista.
module ArbolBin where import Test.QuickCheck import Control.Monad -- necesitamos para la funciones -- liftM, liftM2 data ArbolBin a = Hoja a | Rama (ArbolBin a ) (ArbolBin a ) deriving Show tam :: ArbolBin Int -> Int tam (Hoja a) = 1 tam (Rama i d) = tam i + tam d arbolALista :: ArbolBin Int -> [Int] arbolALista (Hoja x) = [x] arbolALista (Rama i d )= arbolALista i ++ arbolALista d
Una propiedad que nos gustaría que cumpla la función arbolALista es que el tamaño de la lista sea igual al tamaño del árbol.
prop_arbolALista:: ArbolBin Int -> Bool prop_arbolALista t = length (arbolALista t )== tam t instance Arbitrary a => Arbitrary (ArbolBin a) where arbitrary = frequency [ (1,liftM Hoja arbitrary ), (2,liftM2 Rama arbitrary arbitrary )] main = quickCheck prop_arbolALista
El combinador frequency, que vimos anteriormente es del tipo:
frequency :: [(Int, Gen a)] -> Gen a
Su tipo nos dice que recibe una lista de generadores. Como primer parámetro la frecuencia con la que se escogerá cada generador y como segundo el generador en si. En el ejemplo del árbol binario se escogerá dos veces el generador para Rama y solo uno (de cada dos) para el generador de Hoja. En el ejemplo para la inserción para el combinador forAll nos dice que escogerá cuatro veces una lista diferente de vacío([])y una vez una lista vacía ([]).
Algo importante que hay que recalcar es que para utilizar de manera adecuada QuickCheck debemos saber que propiedades necesitamos que cumplan nuestras funciones.
import Test.QuickCheck Definir Propiedades Opcionalmente definir generadores Main > quickCheck pro_definida