Skip to main content

Caso de estudio: Máximo de una lista en Haskell con QuickCheck

⏱ Dedicación recomendada: 0 minutos
Esto considera el contenido visible y relevante, e ignora texto colapsado o marcado como opcional.


r8vnhill/haskell-dibs

Vamos a construir una propiedad para encontrar el mayor valor en una lista de enteros. Al igual que en Kotest, primero definimos la propiedad que se desea probar, asegurándonos de que la función a testear cumpla con los criterios establecidos.

En Haskell, utilizamos QuickCheck para generar automáticamente casos de prueba y verificar una propiedad. En este ejemplo, probamos que el mayor valor en una lista de enteros corresponde al último elemento de la lista cuando está ordenada.

prop_biggest :: [Int] -> Property
prop_biggest xs = not (null xs) ==> biggest xs == last (sort xs)

prop_emptyList :: Property
prop_emptyList = biggest [] === minBound
¿Qué acabamos de hacer?

La propiedad prop_biggest verifica que el valor más grande de una lista no vacía sea el mismo que el último elemento de la lista cuando está ordenada. La función biggest, que se está probando, debe devolver el mayor valor de la lista. La función sort ordena la lista y last devuelve el último elemento, que corresponde al mayor cuando la lista está ordenada. La condición not (null xs) evita que se pruebe la propiedad con listas vacías, ya que no tiene sentido comparar listas vacías en este caso.

¿Por qué foldl en lugar de foldr?
  • foldl max x xs es más eficiente en listas largas porque max es una operación acumulativa que se aplica de izquierda a derecha.
  • foldr max x xs haría que la evaluación ocurra de derecha a izquierda, lo que en listas largas puede llevar a problemas de rendimiento debido a la evaluación diferida (lazy evaluation).
  • foldl en este caso es seguro porque max es estricta y no necesita la evaluación diferida de foldr.

📌 Comparación con Kotest: Generación de Datos y Propiedades

  1. Generación de datos: QuickCheck en Haskell, al igual que Kotest, utiliza generadores para probar múltiples casos. La función Arb.list(Arb.int()) en Kotest se traduce en QuickCheck simplemente como Arbitrary [Int], que genera listas de enteros. Sin embargo, en Haskell, los generadores son implícitos para tipos estándar como listas, lo que facilita la configuración inicial.

  2. Propiedades: Ambas herramientas permiten definir propiedades que se deben cumplir. En QuickCheck, se utiliza el operador ==> para indicar condiciones previas que deben cumplirse antes de ejecutar la prueba, como asegurarse de que la lista no esté vacía, algo que en Kotest se manejaría como un caso de borde.

  3. Pruebas deterministas: Mientras que Kotest utiliza una semilla específica para repetir pruebas fallidas, QuickCheck también puede recibir una semilla para replicar las pruebas, pero el control sobre los generadores de casos puede ser más avanzado en Haskell, gracias a la posibilidad de componer generadores personalizados de manera más fluida.

💻 Implementación de la función biggest

testing\pbt\biggest\Biggest.hs
module Biggest (biggest) where

biggest :: [Int] -> Int
biggest [] = minBound
biggest (x:xs) = foldl max x xs

⚖️ Beneficios y limitaciones

Beneficios

  • Puramente funcional: Haskell, como lenguaje funcional puro, facilita las pruebas basadas en propiedades gracias a la naturaleza inmutable de los datos, lo que simplifica la validación de comportamientos sin preocuparse por efectos secundarios.
  • Generadores avanzados: QuickCheck permite una composición más avanzada y poderosa de generadores, haciendo que las pruebas sean muy expresivas y flexibles. Generadores personalizados se pueden componer de manera sencilla.
  • Tipos y pureza: Gracias a la inferencia de tipos y la pureza de Haskell, se pueden definir propiedades complejas de manera concisa sin preocuparse por efectos secundarios. Esto lo hace ideal para validar comportamientos matemáticos o algoritmos complejos.

Limitaciones

  • Curva de aprendizaje: Aunque QuickCheck es poderoso, tiene una curva de aprendizaje más pronunciada debido a la complejidad del lenguaje y el manejo explícito de efectos secundarios.
  • Manejo manual de efectos secundarios: En Haskell, los efectos secundarios como I/O no son automáticos y requieren uso de monads como IO, lo que puede complicar la escritura de pruebas en algunos casos.
  • Rendimiento: Las pruebas en Haskell pueden ser más lentas si los generadores no están bien optimizados, especialmente al probar propiedades complejas con listas grandes o estructuras de datos avanzadas.

🎯 Conclusiones

📌 Puntos clave

  • QuickCheck y Kotest comparten principios similares → Ambos frameworks permiten definir propiedades en lugar de casos de prueba específicos, generando automáticamente datos para verificar el comportamiento del código.
  • QuickCheck es más expresivo en la composición de generadores → En Haskell, los generadores son implícitos para tipos estándar, mientras que en Kotest se deben definir manualmente.
  • Uso de foldl en la implementación de biggest → Se utiliza foldl en lugar de foldr porque es más eficiente en listas grandes, evitando problemas de evaluación diferida.
  • Pureza funcional y seguridad en Haskell → La ausencia de efectos secundarios facilita la validación de propiedades, asegurando que el código se comporte de manera predecible.
  • Curva de aprendizaje de QuickCheck → Aunque poderoso, su uso requiere familiaridad con Haskell y la manipulación de efectos secundarios mediante monads.

🚀 Lecciones aprendidas

  • Las pruebas basadas en propiedades son una herramienta poderosa → Permiten detectar errores en funciones matemáticas y algoritmos de manera más efectiva que las pruebas unitarias tradicionales.
  • Haskell proporciona garantías adicionales debido a su tipado fuerte → QuickCheck se integra perfectamente con la naturaleza funcional del lenguaje, asegurando que las pruebas sean más predecibles y reutilizables.
  • Comparar herramientas en diferentes lenguajes ayuda a entender sus ventajas y limitaciones → En entornos funcionales como Haskell, las pruebas pueden ser más concisas y expresivas, mientras que en lenguajes como Kotlin, la sintaxis es más familiar para quienes provienen de la programación orientada a objetos.

Al explorar QuickCheck y Kotest, vemos cómo distintas estrategias de Property-Based Testing pueden mejorar la calidad del software, adaptándose a los paradigmas de cada lenguaje.