A menudo encuentro las siguientes declaraciones / argumentos:
- Los lenguajes de programación funcionales puros no permiten efectos secundarios (y, por lo tanto, son de poca utilidad en la práctica porque cualquier programa útil sí tiene efectos secundarios, por ejemplo, cuando interactúa con el mundo externo).
- Los lenguajes de programación funcionales puros no permiten escribir un programa que mantenga el estado (lo que hace que la programación sea muy incómoda porque en muchas aplicaciones se necesita estado).
No soy un experto en lenguajes funcionales, pero esto es lo que he entendido sobre estos temas hasta ahora.
Con respecto al punto 1, puede interactuar con el entorno en lenguajes puramente funcionales, pero debe marcar explícitamente el código (funciones) que introduce efectos secundarios (por ejemplo, en Haskell por medio de tipos monádicos). Además, hasta donde yo sé, la computación por efectos secundarios (actualización destructiva de datos) también debería ser posible (¿usando tipos monádicos?), Aunque no es la forma preferida de trabajar.
Con respecto al punto 2, hasta donde yo sé, puede representar el estado al enhebrar valores a través de varios pasos de cálculo (en Haskell, nuevamente, usando tipos monádicos), pero no tengo experiencia práctica en hacerlo y mi comprensión es bastante vaga.
Entonces, ¿las dos afirmaciones anteriores son correctas en algún sentido o son simplemente conceptos erróneos sobre lenguajes puramente funcionales? Si son ideas falsas, ¿cómo surgieron? ¿Podría escribir un fragmento de código (posiblemente pequeño) que ilustre la forma idiomática de Haskell de (1) implementar efectos secundarios e (2) implementar un cálculo con estado?
fuente
Respuestas:
A los fines de esta respuesta, defino "lenguaje puramente funcional" para significar un lenguaje funcional en el que las funciones son referencialmente transparentes, es decir, llamar a la misma función varias veces con los mismos argumentos siempre producirá los mismos resultados. Esta es, creo, la definición habitual de un lenguaje puramente funcional.
La forma más fácil de lograr la transparencia referencial sería, de hecho, no permitir los efectos secundarios y, de hecho, hay idiomas en los que ese es el caso (en su mayoría, dominios específicos). Sin embargo, ciertamente no es la única forma y la mayoría de los lenguajes puramente funcionales (Haskell, Clean, ...) permiten efectos secundarios.
También creo que decir que un lenguaje de programación sin efectos secundarios es poco útil en la práctica no es realmente justo, ciertamente no para lenguajes específicos de dominio, pero incluso para lenguajes de propósito general, me imagino que un lenguaje puede ser bastante útil sin proporcionar efectos secundarios . Quizás no para aplicaciones de consola, pero creo que las aplicaciones GUI pueden implementarse sin efectos secundarios en, por ejemplo, el paradigma funcional reactivo.
Eso es un poco más que simplificarlo. El simple hecho de tener un sistema donde las funciones de efectos secundarios deban marcarse como tales (similar a la corrección constante en C ++, pero con efectos secundarios generales) no es suficiente para garantizar la transparencia referencial. Debe asegurarse de que un programa nunca pueda llamar a una función varias veces con los mismos argumentos y obtener resultados diferentes. Podrías hacer eso haciendo cosas como
readLine
sea algo que no sea una función (eso es lo que hace Haskell con la mónada IO) o podría hacer que sea imposible llamar a las funciones de efecto secundario varias veces con el mismo argumento (eso es lo que hace Clean). En el último caso, el compilador se aseguraría de que cada vez que llame a una función de efectos secundarios, lo haga con un argumento nuevo, y rechazaría cualquier programa en el que pase dos veces el mismo argumento a una función de efectos secundarios.Una vez más, un lenguaje puramente funcional podría no permitir el estado mutable, pero ciertamente es posible ser puro y tener un estado mutable, si lo implementa de la misma manera que describí con los efectos secundarios anteriores. El estado realmente mutable es solo otra forma de efectos secundarios.
Dicho esto, los lenguajes de programación funcionales definitivamente desalientan el estado mutable, especialmente los puros. Y no creo que eso haga que la programación sea incómoda, sino todo lo contrario. A veces (pero no con tanta frecuencia) el estado mutable no se puede evitar sin perder rendimiento o claridad (es por eso que los lenguajes como Haskell tienen facilidades para el estado mutable), pero la mayoría de las veces sí se puede.
Creo que muchas personas simplemente leen "una función debe producir el mismo resultado cuando se llama con los mismos argumentos" y concluyen que no es posible implementar algo como
readLine
o código que mantenga un estado mutable. Por lo tanto, simplemente no son conscientes de los "trucos" que los lenguajes puramente funcionales pueden usar para introducir estas cosas sin romper la transparencia referencial.También el estado mutable es muy desalentador en los lenguajes funcionales, por lo que no es un gran salto suponer que no está permitido en absoluto en los lenguajes puramente funcionales.
Aquí hay una aplicación en Pseudo-Haskell que le pide al usuario un nombre y lo saluda. Pseudo-Haskell es un lenguaje que acabo de inventar, que tiene el sistema IO de Haskell, pero usa una sintaxis más convencional, nombres de funciones más descriptivos y no tiene
do
anotación (ya que eso solo distraería de cómo funciona exactamente la mónada IO):La pista aquí es que
readLine
es un valor de tipoIO<String>
ycomposeMonad
es una función que toma un argumento de tipoIO<T>
(para algún tipoT
) y otro argumento que es una función que toma un argumento de tipoT
y devuelve un valor de tipoIO<U>
(para algún tipoU
).print
es una función que toma una cadena y devuelve un valor de tipoIO<void>
.Un valor de tipo
IO<A>
es un valor que "codifica" una acción dada que produce un valor de tipoA
.composeMonad(m, f)
produce un nuevoIO
valor que codifica la acción dem
seguido por la acción def(x)
, dondex
es el valor que produce al realizar la acción dem
.El estado mutable se vería así:
Aquí
mutableVariable
hay una función que toma valor de cualquier tipoT
y produce aMutableVariable<T>
. La funcióngetValue
tomaMutableVariable
y devuelve unIO<T>
que produce su valor actual.setValue
toma unaMutableVariable<T>
y unaT
y devuelve unaIO<void>
que establece el valor.composeVoidMonad
es lo mismo quecomposeMonad
excepto que el primer argumento es unIO
que no produce un valor sensible y el segundo argumento es otra mónada, no una función que devuelve una mónada.En Haskell hay algo de azúcar sintáctica, que hace que esta prueba sea menos dolorosa, pero aún es obvio que el estado mutable es algo que el lenguaje realmente no quiere que hagas.
fuente
counter
, es decirincreaseCounter(counter)
?main
será la que realmente se ejecute. Aparte de devolver un IO desdemain
allí, no hay forma de ejecutarIO
acciones (sin usar funciones terriblemente malvadas que tienenunsafe
en su nombre).IO
valores destructivos . No entendí si se refiere a la coincidencia de patrones, es decir, al hecho de que puede deconstruir valores de un tipo de datos algebraicos, pero uno no puede usar la coincidencia de patrones para hacer esto con losIO
valores.En mi humilde opinión, estás confundido porque hay una diferencia entre un lenguaje puro y una función pura . Comencemos con la función. Una función es pura si (con la misma entrada) siempre devuelve el mismo valor y no causa ningún efecto secundario observable. Ejemplos típicos son funciones matemáticas como f (x) = x * x. Ahora considere una implementación de esta función. Sería puro en la mayoría de los idiomas, incluso en aquellos que generalmente no se consideran lenguajes funcionales puros, por ejemplo, ML. Incluso un método Java o C ++ con este comportamiento puede considerarse puro.
Entonces, ¿qué es un lenguaje puro? Estrictamente hablando, uno podría esperar que un lenguaje puro no le permita expresar funciones que no son puras. Llamemos a esto la definición idealista de un lenguaje puro. Tal comportamiento es altamente deseable. ¿Por qué? Bueno, lo bueno de un programa que consiste solo de funciones puras es que puede reemplazar la aplicación de función con su valor sin cambiar el significado del programa. Esto hace que sea muy fácil razonar sobre los programas porque una vez que conoce el resultado, puede olvidar la forma en que se calculó. La pureza también puede permitir que el compilador realice ciertas optimizaciones agresivas.
¿Y qué si necesitas un estado interno? Puede imitar el estado en un lenguaje puro simplemente agregando el estado antes del cálculo como parámetro de entrada y el estado después del cálculo como parte del resultado. En lugar de
Int -> Bool
obtener algo comoInt -> State -> (Bool, State)
. Simplemente hace explícita la dependencia (que se considera una buena práctica en cualquier paradigma de programación). Por cierto, hay una mónada que es una forma particularmente elegante de combinar tales funciones de imitación de estado en funciones de imitación de estado más grandes. De esta manera definitivamente puedes "mantener el estado" en un lenguaje puro. Pero tienes que hacerlo explícito.Entonces, ¿esto significa que puedo interactuar con el exterior? Después de todo, un programa útil debe interactuar con el mundo real para ser útil. Pero la entrada y la salida obviamente no son puras. Escribir un byte específico en un archivo específico podría estar bien por primera vez. Pero ejecutar exactamente la misma operación por segunda vez podría devolver un error porque el disco está lleno. Claramente, no existe un lenguaje puro (en el significado idealista) que pueda escribir en un archivo.
Entonces nos enfrentamos a un dilema. Queremos funciones principalmente puras, pero algunos efectos secundarios son absolutamente necesarios y esos no son puros. Ahora, una definición realista de un lenguaje puro sería que tiene que haber algún medio para separar las partes puras de las otras partes. El mecanismo debe garantizar que ninguna operación impura se cuela en las partes puras.
En Haskell esto se hace con el tipo IO. No puede destruir un resultado IO (sin mecanismos inseguros). Por lo tanto, solo puede procesar resultados de E / S con funciones definidas en el propio módulo de E / S. Afortunadamente, hay un combinador muy flexible que le permite tomar un resultado IO y procesarlo en una función siempre que esa función devuelva otro resultado IO. Este combinador se llama enlace (o
>>=
) y tiene el tipoIO a -> (a -> IO b) -> IO b
. Si generaliza este concepto, llega a la clase mónada y resulta que IO es una instancia de él.fuente
unsafe
su nombre) no cumple con su definición idealista. No hay funciones impuras en Haskell (nuevamente ignorandounsafePerformIO
y co.).readFile
ywriteFile
siempre devolverá el mismoIO
valor, dados los mismos argumentos. Entonces, por ejemplo, los dos fragmentos de códigolet x = writeFile "foo.txt" "bar" in x >> x
ywriteFile "foo.txt" "bar" >> writeFile "foo.txt" "bar"
harán lo mismo.IO Something
? Si es así, es perfectamente posible llamar a una función IO dos veces con el mismo argumento:putStrLn "hello" >> putStrLn "hello"
aquí ambas llamadasputStrLn
tienen el mismo argumento. Por supuesto, eso no es un problema porque, como dije antes, ambas llamadas darán como resultado el mismo valor de IO.writeFile "foo.txt" "bar"
no puede causar un error porque evaluar la llamada a la función no ejecuta la acción. Si está diciendo que en mi ejemplo anterior, la versión conlet
solo tiene una oportunidad de causar una falla de E / S mientras que la versión sinlet
tiene dos, está equivocado. Ambas versiones tienen dos oportunidades para una falla de E / S. Como lalet
versión evalúa la llamadawriteFile
solo una vez, mientras que la versión sin lalet
evalúa dos veces, puede ver que no importa con qué frecuencia se llama a la función. Solo importa con qué frecuencia el resultado ...putStrLn
función toma exactamente un argumento, que es de tipoString
. Si no me creen, miren en su tipo:String -> IO ()
. Ciertamente no toma ningún argumento de tipoIO
, produce un valor de ese tipo.