Nota: Haskell es conocido por ser poco amigable, por ende te recomiendo tener algunas bases sobre el lenguaje. Haré mi mejor esfuerzo para dar a entender conceptos muy técnicos y específicos de Haskell.

Si alguna vez pasaste por un curso de matemáticas en la universidad, seguramente te cruzaste con los conjuntos. Los conjuntos son muy útiles para los programadores dado que permiten definir una colección de información que posteriormente puede ser utilizada para un montón de cosas, dependiendo el caso claro está; por ejemplo, dada una entrada de datos, nos gustaría definir múltiples conjuntos que nos ayuden a agrupar dichos datos y enviarlos a la siguiente instrucción; algo así como clasificarlos.

Normalmente, no es muy práctico definir a un conjunto expresando cada uno de todos sus elementos dado que existen los conjuntos infinitos, como por ejemplo, el conjunto de los números impares. Una mejor manera para definir los elementos que un conjunto puede albergar es utilizar algo llamado set comprehensions, lo que en español sería algo parecido a conjunto de comprensiones. Los set comprehensions no son más que una formula contra la cual un elemento es (o no) modificado y posteriormente comparado; si la comparación del elemento es verdadera, significa que ese elemento pertenece al conjunto que define ese conjunto de comprensión. No te asustes, pongamos un ejemplo sencillo.

Problemática

Imagina que necesitas un conjunto con los primeros 50 números naturales pares. Si eres una matemático seguramente expresaras tu solución utilizando un conjunto de comprensión; algo más o menos así: $$E = \{2 \cdot x | x \in \mathbb N, x \le 50 \}$$ En palabras mortales, la expresión de arriba significa que el conjunto E (Even numbers) está formado por los primeros 50 números de N (el conjunto de los números naturales) donde x sea menor a 50. Por ejemplo, los primeros 10 números naturales son: $$\mathbb N = \{1, 2, 3, 4, 5, 6, 7, 8, 9, 10 \ldots\}$$ Si utilizamos la expresión definida anteriormente y tabulamos los resultados, obtendremos el siguiente resultado: $$E = \{2, 4, 6, 8, 10, 12, 14, 16, 18, 20 \ldots\}$$ Parece que lo hemos logrado. Hemos definido un conjunto de comprensión que permite definir los números pares. Ahora viene la parte divertida: hacerlo en código y para más diversión aún, hacerlo en Haskell.

Solución en Haskell

En programación existe un concepto muy parecido a los conjuntos de comprensión solo que en vez de conjuntos, los programadores tendemos a utilizar listas. Es por eso en programación, este concepto es llamado list comprehension.

Dependiendo del lenguaje estas listas tienden a tener una que otra propiedad de más (o de menos). En el caso de Haskell, todas las variables son inmutables. Es decir, una vez que una variable ha sido definida, esa variable no podrá volver a cambiar su valor en tiempo de ejecución.

En Haskell, la sintaxis de una lista de comprensión es bastante parecida a la como lo hacen en el mundo de las matemáticas. La solución de más arriba se puede expresar de las siguiente manera utilizando Haskell:

[x * 2 | x <- [1..50]]

Es bastante parecido pero hay unos detalles que pueden confundir a cualquiera. Por ejemplo:

  • ¿Cuándo y dónde fue definida x?
  • ¿Dónde está la condición para que sean los primeros 50?

Las respuestas a estas preguntas se encuentran en la naturaleza de Haskell y en cómo funcionan las listas de Haskell. Si estás leyendo este artículo asumiré que tienes cierto conocimiento básico de Haskell por ende no entraré mucho en detalles pero si te interesa, te recomiendo leer sobre list ranges.

Dado que la expresión [1..50] genera todos los números naturales entre 1 y 50, no hace falta poner explícitamente el condicional dado que todo el conjunto de entrada ya contiene los datos necesarios. Por otra parte, la definición de la variable x se hace en la primera parte de la lista de comprensión.

Sintaxis de una List Comprehension

Como puedes observar en el snippet de código más arriba, esa list comprehension se compone de dos partes y estas están dividadas por un pipe (la linea vertical). La primera parte, se llama la función de salida o output function; del otro lado podemos encontrar dos elementos: un conjunto de entrada o input (los primeros 50 números naturales) y un predicado (el condicional). Como mencioné antes, en esa lista de comprensión el predicado se encuentra de manera implicita.

Si queremos un ejemplo más explicito, podemos agregarle complejidad al problema. Digamos que ahora necesitas los números pares que sean mayor o igual a 10. Para ello, podemos agregar explícitamente la condicional agregando una coma después de nuestro conjunto de entrada y la respectiva condición:

[x * 2 | x <- [1..50], x >= 10]

Ahora sí que es bastante explícito. Por obvias razones, el conjunto resultante tendrá menos elementos dado que omitiremos aquellos que sean menores a 10. Ahora imagina que necesitas todos los números entre 50 y 100 cuyo residuo después de dividirlo entre tres sea uno.

[ x | x <- [50..100], x `mod` 3 == 1]

Como puedes observar, las listas de comprensión son una herramienta muy poderosa e incluso las podemos hacer más poderosas. ¿Recuerdas cómo se llama la parte antes del pipe? Tiene el nombre function por algo, te mostraré:

if> 75 then "Mostaza" else "Mayonesa" | x <- [50..100], x `mod` 3 == 1]

El snippet de código anterior intercambia todos los números mayores de 75 por la palabra Mostaza y todos demás por la palabra Mayonesa, dando como resultado una lista de Strings. Se le llama output function porque puedes poner más código Haskell dentro de este espacio y dado que las funciones en Haskell son ciudadanos de primera clase, muy seguramente quieras poner funciones funciones ahí (en el primero ejemplo, la expresión x * 2 es una función). Es más, ¡incluso puedes poner listas de comprensión dentro de otras listas de comprensión!

Imagina que tienes una lista de listas y quieres obtener una lista de listas que solo tengan números impares:

let listaDeListas = [[1,3,5,2,3,1,2,4,5], [1,2,3,4,5,6,7,8,9], [1,2,4,2,1,6,3,1,3,2,3,6]]

Para lograrlo, necesitarás primero una lista de comprensión que recorra la lista de listas y posteriormente, dentro de cada lista interna deberás filtrar aquellos elementos impares. Puede que ya te hayas dado cuenta que estamos haciendo nesting o anidando, es exactamente el mismo concepto que alguna vez aprendiste en Java anidando un ciclo for dentro de otro.

En código, la solución se vería así:

[ [ x | x <- listaInterna, odd x ] | listaInterna <- listaDeListas]

La anterior expresión se lee de afuera hacia adentro. Como mencioné antes, la comprensión de lista más exterior está recorriendo la lista de listas; el resultado de ese recorrido, es expresado en la variable listaInterna que a su vez también es una lista. Posteriormente, la lista de comprensión más adentro utiliza como entrada la variable listaInterna y el predicado odd x (odd es una función nativa de Haskell para determinar si un número es impar). Eso es todo, sencillo ¿Verdad?

Conclusión

Las listas de comprensión son un feature muy poderoso de Haskell y sus casos de usos están limitados por la imaginación del programador. Normalmente, cuando una lista de comprensión se vuelve muy larga y compleja de leer, se recomienda desmenuzarla en múltiples líneas.