3 puntos por GN⁺ 2025-01-20 | 2 comentarios | Compartir por WhatsApp

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 Int representa “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 do no 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ón IO () 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_side crea un efecto secundario que pasa el resultado del dado a print_side para 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 do se construye con estos dos operadores: *> y >>=.
  • La sintaxis do se 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 x crea “una acción que produce el valor x como resultado, sin ningún efecto secundario adicional”.
  • Ejemplo: loaded_die = pure 4 crea un IO Int que 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 aplica length para calcular su longitud.

liftA2, liftA3, …

  • Funciones como liftA2 y liftA3 crean 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 log y después ejecutarlas todas de una vez con sequenceA.
  • Incluso efectos secundarios que se repiten infinitamente (por ejemplo, repeat (randomRIO(1,6))) pueden guardarse en una lista y luego ejecutarse con sequenceA tomando solo la cantidad necesaria con take 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, sequenceA es igual a traverse id, y traverse_ es la versión que descarta los resultados.

for

  • for ofrece la misma funcionalidad que traverse, 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 State en lugar de IO para 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 evalState y 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 *>, pure y traverse.
  • 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

  • fmap tiene 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 fmap a una lista, se aplica la función a todos sus elementos; si se aplica a IO, 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

  • Foldable es una estructura cuyos elementos pueden recorrerse y procesarse.
  • Traversable es una estructura que, además de recorrerse, puede reconstruirse con nuevos elementos manteniendo la misma forma.
  • Para que sequenceA o traverse puedan reunir valores preservando la estructura original, esa estructura debe ser Traversable.
  • Estructuras de datos como árboles o Set pueden 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

 
bbulbum 2025-01-21

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...

 
GN⁺ 2025-01-20
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

    • Si no usas Haskell durante un mes, necesitas volver a estudiar operadores como >>= y >> para mantener la productividad
    • Si estudias los conceptos de Haskell por tu cuenta, sin conversar con otras personas, resulta difícil
  • Haskell ayuda a mejorar la programación imperativa

    • Se puede eliminar código boilerplate usando efectos y patrones de primera clase
    • Gracias a la seguridad de tipos, se puede escribir código relativamente libre de errores con rapidez
  • La versión generalizada de traverse/mapM funciona no solo para listas, sino para cualquier tipo Traversable, y es muy útil

    • Se puede usar con la forma traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
    • En otros lenguajes, había que escribir manualmente mucho código para lograr un efecto similar
  • Haskell tiene mónadas potentes, y eso hace que Haskell sea más procedimental

    • Se pueden usar variables intermedias dentro de bloques do
  • Entre 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

    • Trabajar con la mónada IO es complejo, y se vuelve aún más complejo cuando se quiere usar otros tipos de mónadas
  • >> es el nombre antiguo de <i>>, y ambos operadores son asociativos por la izquierda

    • >> está definido como infixl 1 y <i>> como infixl 4, por lo que <i>> tiene una asociación más fuerte que >>
  • IO a y a de Haskell pueden sentirse parecidos a lo asíncrono y lo síncrono

    • El primero devuelve una promesa/futuro que hay que esperar
  • En otros lenguajes, se puede hacer IO simple con funciones como console.log("abc")

    • Existe la duda de si hay alguna diferencia con el IO de Haskell
  • Quienes no han probado Haskell pueden sentir que el Haskell real con extensiones de GHC es demasiado complejo

    • Eso puede hacer que pierdan el interés en Haskell