Si los lenguajes de programación funcionales no pueden guardar ningún estado, ¿cómo hacen cosas simples como leer la entrada de un usuario (me refiero a cómo la "almacenan"), o almacenar cualquier dato para el caso?
Como pudo deducir, la programación funcional no tiene estado, pero eso no significa que no pueda almacenar datos. La diferencia es que si escribo una declaración (Haskell) en la línea de
let x = func value 3.14 20 "random"
in ...
Tengo la garantía de que el valor de x
es siempre el mismo en el ...
: nada puede cambiarlo. De manera similar, si tengo una función f :: String -> Integer
(una función que toma una cadena y devuelve un número entero), puedo estar seguro de que f
no modificará su argumento, ni cambiará ninguna variable global, ni escribirá datos en un archivo, etc. Como dijo sepp2k en un comentario anterior, esta no mutabilidad es realmente útil para razonar sobre programas: usted escribe funciones que pliegan, combinan y mutilan sus datos, devolviendo nuevas copias para que pueda encadenarlas juntas, y puede estar seguro de que ninguna de esas llamadas a funciones pueden hacer algo "dañino". Sabes que x
es siempre x
, y no tienes que preocuparte de que alguien haya escrito x := foo bar
en algún lugar entre la declaración dex
y su uso, porque eso es imposible.
Ahora, ¿qué pasa si quiero leer la entrada de un usuario? Como dijo KennyTM, la idea es que una función impura es una función pura que se transmite al mundo entero como argumento y devuelve tanto su resultado como el mundo. Por supuesto, en realidad no quieres hacer esto: por un lado, es horriblemente torpe, y por otro, ¿qué sucede si reutilizo el mismo objeto mundial? Entonces esto se abstrae de alguna manera. Haskell lo maneja con el tipo IO:
main :: IO ()
main = do str <- getLine
let no = fst . head $ reads str :: Integer
...
Esto nos dice que main
es una acción IO que no devuelve nada; ejecutar esta acción es lo que significa ejecutar un programa Haskell. La regla es que los tipos IO nunca pueden escapar de una acción IO; en este contexto, introducimos esa acción usando do
. Por tanto, getLine
devuelve an IO String
, que se puede considerar de dos formas: primero, como una acción que, cuando se ejecuta, produce una cadena; segundo, como una cadena que está "contaminada" por IO, ya que se obtuvo de manera impura. El primero es más correcto, pero el segundo puede ser más útil. El <-
toma elString
salida de la IO String
y lo almacena en str
-pero ya que estamos en una acción IO, tendremos que envuelve una copia de seguridad, por lo que no pueden "escapar". La siguiente línea intenta leer un entero ( reads
) y toma la primera coincidencia exitosa (fst . head
); todo esto es puro (sin IO), así que le damos un nombre con let no = ...
. Luego podemos usar ambos no
y str
en el ...
. Por lo tanto, hemos almacenado datos impuros (desde getLine
adentro str
) y datos puros ( let no = ...
).
Este mecanismo para trabajar con IO es muy poderoso: le permite separar la parte algorítmica pura de su programa del lado impuro de interacción con el usuario, y hacer cumplir esto a nivel de tipo. Es posible que su minimumSpanningTree
función no pueda cambiar algo en otro lugar de su código, o escribir un mensaje a su usuario, y así sucesivamente. Es seguro.
Esto es todo lo que necesita saber para usar IO en Haskell; si eso es todo lo que quieres, puedes parar aquí. Pero si quieres entender por qué funciona, sigue leyendo. (Y tenga en cuenta que estas cosas serán específicas de Haskell; otros lenguajes pueden elegir una implementación diferente).
Así que esto probablemente parecía una trampa, agregando de alguna manera impureza al Haskell puro. Pero no lo es, resulta que podemos implementar el tipo IO completamente dentro de Haskell puro (siempre que se nos dé el RealWorld
). La idea es esta: una acción IO IO type
es lo mismo que una función RealWorld -> (type, RealWorld)
, que toma el mundo real y devuelve tanto un objeto de tipo type
como el modificado RealWorld
. Luego definimos un par de funciones para que podamos usar este tipo sin volvernos locos:
return :: a -> IO a
return a = \rw -> (a,rw)
(>>=) :: IO a -> (a -> IO b) -> IO b
ioa >>= fn = \rw -> let (a,rw') = ioa rw in fn a rw'
La primera nos permite hablar de acciones IO que no hacen nada: return 3
es una acción IO que no consulta el mundo real y simplemente regresa 3
. El >>=
operador, pronunciado "bind", nos permite ejecutar acciones IO. Extrae el valor de la acción IO, lo pasa al mundo real a través de la función y devuelve la acción IO resultante. Tenga en cuenta que>>=
hace cumplir nuestra regla de que los resultados de las acciones de IO nunca pueden escapar.
Luego podemos convertir lo anterior main
en el siguiente conjunto ordinario de aplicaciones de funciones:
main = getLine >>= \str -> let no = (fst . head $ reads str :: Integer) in ...
El tiempo de ejecución de Haskell comienza main
con la inicialRealWorld
, ¡y estamos listos! Todo es puro, solo tiene una sintaxis elegante.
[ Editar: como señala @Conal , esto no es realmente lo que Haskell usa para hacer IO. Este modelo se rompe si agrega simultaneidad o, de hecho, cualquier forma de que el mundo cambie en medio de una acción de IO, por lo que sería imposible para Haskell usar este modelo. Es preciso solo para cálculos secuenciales. Por lo tanto, puede ser que la IO de Haskell sea un poco esquiva; incluso si no lo es, ciertamente no es tan elegante. Según la observación de @ Conal, vea lo que dice Simon Peyton-Jones en Tackling the Awkward Squad [pdf] , sección 3.1; presenta lo que podría equivaler a un modelo alternativo en este sentido, pero luego lo abandona por su complejidad y toma un rumbo diferente.]
Una vez más, esto explica (prácticamente) cómo funciona IO y la mutabilidad en general en Haskell; si esto es todo lo que quieres saber, puedes dejar de leer aquí. Si desea una última dosis de teoría, siga leyendo, pero recuerde, en este punto, ¡nos hemos alejado mucho de su pregunta!
Así que una última cosa: resulta que esta estructura, un tipo paramétrico con return
y >>=
, es muy general; se llama mónada, y la do
notación return
, y >>=
funciona con cualquiera de ellos. Como vio aquí, las mónadas no son mágicas; todo lo que es mágico es que los do
bloques se convierten en llamadas a funciones. El RealWorld
tipo es el único lugar en el que vemos magia. Los tipos como []
, el constructor de listas, también son mónadas y no tienen nada que ver con código impuro.
Ahora sabe (casi) todo sobre el concepto de mónada (excepto algunas leyes que deben cumplirse y la definición matemática formal), pero le falta la intuición. Hay una cantidad ridícula de tutoriales de mónadas en línea; Me gusta este , pero tienes opciones. Sin embargo, esto probablemente no le ayudará ; la única forma real de obtener la intuición es mediante una combinación de usarlos y leer un par de tutoriales en el momento adecuado.
Sin embargo, no necesitas esa intuición para entender IO . Entender las mónadas en su totalidad es la guinda del pastel, pero puedes usar IO ahora mismo. Podrías usarlo después de que te mostré el primeromain
función. ¡Incluso puede tratar el código IO como si estuviera en un lenguaje impuro! Pero recuerde que hay una representación funcional subyacente: nadie está haciendo trampa.
(PD: Perdón por la duración. Fui un poco más lejos).