Haskell: un excelente lenguaje procedural
(entropicthoughts.com)Tratar los efectos secundarios como valores de primera clase
- En Haskell, los efectos secundarios (por ejemplo, generar números aleatorios o imprimir salida) se tratan como si fueran “valores de primera clase (first class value)”.
- Es decir, una llamada de función que genera un efecto secundario como
randomRIO(1, 6)no devuelve el resultado en sí, sino un “objeto que describe una acción que se ejecutará en algún momento”. - Ese objeto generará un valor aleatorio cuando realmente se ejecute, pero antes de eso solo contiene el plan de ejecución.
- Un tipo como
IO Intrepresenta “una acción que, cuando se ejecuta de verdad, produce un Int”; no se ejecuta inmediatamente al llamarla, sino después, cuando haga falta. - Gracias a esta característica, a diferencia de los lenguajes procedurales tradicionales donde “llamada a función = ejecución inmediata”, en Haskell se pueden combinar efectos secundarios y ejecutarlos realmente más tarde.
De-mystifying do blocks
- El bloque
dono es una sintaxis mágica; en realidad, está compuesto básicamente por dos operadores: uno para enlazar (bind) efectos secundarios y otro para ejecutarlos en secuencia (then).
then
- El operador
*>ejecuta el efecto secundario de la izquierda, descarta su valor de resultado y luego ejecuta el efecto secundario de la derecha. - Por ejemplo,
putStr "hello" *> putStrLn "world"crea una sola acciónIO ()que combina las dos salidas en orden. - Cuando escribes varias líneas en un bloque
do, internamente se usa este tipo de operador de ejecución secuencial.
bind
- El operador
>>=ejecuta el efecto secundario de la izquierda y pasa el valor obtenido a la función de la derecha. - Ejemplo:
randomRIO(1, 6) >>= print_sidecrea un efecto secundario que pasa el resultado del dado aprint_sidepara imprimirlo. - En un bloque
do, el patrón<-es la forma práctica de expresar este operador.
Two operators are all of do blocks
- Al final, el bloque
dose construye con estos dos operadores:*>y>>=. - La sintaxis
dose usa mucho por legibilidad y comodidad, pero para aprovechar mejor las ventajas de Haskell conviene usar también funciones más ricas para combinar efectos secundarios.
Functions that operate on side effects
- En la biblioteca estándar existen varias funciones para manejar efectos secundarios de formas más variadas.
pure
pure xcrea “una acción que produce el valor x como resultado, sin ningún efecto secundario adicional”.- Ejemplo:
loaded_die = pure 4crea unIO Intque siempre devuelve 4.
fmap
- Con la forma
fmap :: (a -> b) -> IO a -> IO b, crea una acción que aplica una función pura al valor resultado de un efecto secundario para producir un nuevo valor resultado. - Ejemplo:
length <$> getEnv "HOME"permite crear una acción que obtiene una variable de entorno y luego aplicalengthpara calcular su longitud.
liftA2, liftA3, …
- Funciones como
liftA2yliftA3crean un nuevo efecto secundario combinando los resultados de varios efectos secundarios mediante una sola función pura. - Ejemplo:
liftA2 (+) (randomRIO(1,6)) (randomRIO(1,6))crea un efecto secundario que suma los valores de dos dados. - También se puede hacer lo mismo combinando
<$>y<*>.
Intermission: what’s the point?
- Este enfoque puede parecer una capacidad simple que también existe en otros lenguajes, pero en Haskell tiene la ventaja de que puedes extraer una acción con efectos secundarios a una variable o recombinarla en cualquier momento sin que cambien ni el momento de ejecución ni el resultado.
- Al tratar los efectos secundarios de forma independiente, hay menos confusión al refactorizar código y es posible una reutilización segura basada en el razonamiento ecuacional (equational reasoning).
sequenceA
sequenceA [IO a] -> IO [a]convierte “una lista de acciones con efectos secundarios” en “una sola acción con efecto secundario que produce una lista de resultados”.- Ejemplo: se puede reunir una lista de acciones
logy después ejecutarlas todas de una vez consequenceA. - Incluso efectos secundarios que se repiten infinitamente (por ejemplo,
repeat (randomRIO(1,6))) pueden guardarse en una lista y luego ejecutarse consequenceAtomando solo la cantidad necesaria contake n.
Interlude: convenience functions
void,sequenceA_,replicateM,replicateM_y otras funciones son convenientes cuando no se usa el valor resultado o cuando se quiere repetir una ejecución.- Ejemplo: con
replicateM_ 500 (putStrLn "I will not cheat again.")se puede ejecutar un efecto secundario muchas veces sin contar manualmente las repeticiones.
traverse
traverse :: (a -> IO b) -> [a] -> IO [b]crea una acción que aplica una función con efectos secundarios a cada elemento de una lista y reúne los resultados en una lista.- En realidad,
sequenceAes igual atraverse id, ytraverse_es la versión que descarta los resultados.
for
-
forofrece la misma funcionalidad quetraverse, pero recibe los argumentos en el orden inverso. -
Ejemplo: en la forma
for numbers $ \n -> ...se puede expresar de manera natural algo parecido a un “bucle for”. -
Gracias a estas combinaciones, repeticiones, recorridos y transformaciones de estructuras de datos que en otros lenguajes requieren sintaxis especial, en Haskell pueden implementarse componiendo funciones de biblioteca.
Leaning into the first classiness of effects
- Si en Haskell se aprovecha activamente que los efectos secundarios son valores de primera clase, es posible reducir duplicación de código o mejorar la estructura.
- Por ejemplo, en una lógica de factorización prima de números grandes con caché, se puede usar
Stateen lugar deIOpara construir una estructura donde “existen efectos secundarios, pero no afectan al exterior”. - Los efectos secundarios estructurados de esta manera se aplican solo donde hacen falta, y el resto del código puede mantenerse como funciones puras, logrando al mismo tiempo seguridad y flexibilidad.
- Al final, con
evalStatey similares, se puede ejecutar realmente el efecto secundario y convertir el resultado en un valor puro.
Things you never need to care about
- Varios nombres heredados de épocas antiguas de Haskell (
>>,return,mapM, etc.) pueden sustituirse por funciones actuales como*>,pureytraverse. - Provienen de “nombres viejos o diseños centrados en mónadas”, y hoy se recomienda más un enfoque basado en Applicative o en el Functor más general.
Appendix A: Avoiding success and uselessness
- La frase “Haskell avoids success” significa que “el lenguaje no sacrifica sus valores fundamentales por popularidad o conveniencia”.
- “Haskell is useless” al principio parecía indicar que era un lenguaje que no permitía hacer absolutamente nada porque solo aceptaba funciones puras completas, pero luego adquirió utilidad al introducirse la forma de tratar los efectos secundarios como algo ‘de primera clase’.
Appendix B: Why fmap maps over both side effects and lists
fmaptiene una forma muy general (Functor f => (a -> b) -> f a -> f b), por lo que se aplica en común a muchos tipos de contenedor o de efecto secundario, como listas, Maybe e IO.- Si se aplica
fmapa una lista, se aplica la función a todos sus elementos; si se aplica aIO, se aplica la función al valor resultado. - Por eso, cualquier “estructura a la que se le puede aplicar una función” se llama Functor.
Appendix C: Foldable and Traversable
Foldablees una estructura cuyos elementos pueden recorrerse y procesarse.Traversablees una estructura que, además de recorrerse, puede reconstruirse con nuevos elementos manteniendo la misma forma.- Para que
sequenceAotraversepuedan reunir valores preservando la estructura original, esa estructura debe serTraversable. - Estructuras de datos como árboles o
Setpueden cambiar su estructura según los valores, por lo que se distingue entre los casos donde solo es posible recorrer (Foldable) y los casos donde realmente se puede reconstruir la estructura (Traversable). - Según la necesidad, también se puede manejar los efectos secundarios con flexibilidad convirtiendo primero a lista y usando luego
traverse, entre otros enfoques.
2 comentarios
Cuando veo Reddit, aparecen muchos anuncios... pero desde el nombre ya se siente como una pequeña barrera psicológica.
Da la impresión de ser un lenguaje muy difícil y potente...
Opiniones en Hacker News
El sistema de tipos de Haskell tiene cierta complejidad en comparación con otros lenguajes populares. En particular, operadores como
*>,<*>y<*elevan la curva de aprendizaje a lo largo de toda la base de código>>=y>>para mantener la productividadHaskell ayuda a mejorar la programación imperativa
La versión generalizada de
traverse/mapMfunciona no solo para listas, sino para cualquier tipoTraversable, y es muy útiltraverse :: Applicative f => (a -> f b) -> t a -> f (t b)Haskell tiene mónadas potentes, y eso hace que Haskell sea más procedimental
doEntre el software escrito en Haskell está ImplicitCAD
El código de Haskell se lee como si fuera un lenguaje procedimental, pero ofrece ventajas al trabajar con funciones con efectos secundarios
>>es el nombre antiguo de<i>>, y ambos operadores son asociativos por la izquierda>>está definido comoinfixl 1y<i>>comoinfixl 4, por lo que<i>>tiene una asociación más fuerte que>>IO ayade Haskell pueden sentirse parecidos a lo asíncrono y lo síncronoEn otros lenguajes, se puede hacer IO simple con funciones como
console.log("abc")Quienes no han probado Haskell pueden sentir que el Haskell real con extensiones de GHC es demasiado complejo