QuickCheck

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:

Combinadores provistos por QuickCheck

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