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.
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).
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.
Con respecto al punto 1, puede interactuar con el entorno en lenguajes puramente funcionales, pero debe marcar explícitamente el código (funciones) que los introduce (por ejemplo, en Haskell mediante tipos monádicos).
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 comoreadLine
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.
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).
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.
Si son ideas falsas, ¿cómo surgieron?
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.
¿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?
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):
greet(name) = print("Hello, " ++ name ++ "!")
main = composeMonad(readLine, greet)
La pista aquí es que readLine
es un valor de tipo IO<String>
y composeMonad
es una función que toma un argumento de tipo IO<T>
(para algún tipo T
) y otro argumento que es una función que toma un argumento de tipo T
y devuelve un valor de tipo IO<U>
(para algún tipo U
). print
es una función que toma una cadena y devuelve un valor de tipo IO<void>
.
Un valor de tipo IO<A>
es un valor que "codifica" una acción dada que produce un valor de tipo A
. composeMonad(m, f)
produce un nuevo IO
valor que codifica la acción de m
seguido por la acción de f(x)
, donde x
es el valor que produce al realizar la acción de m
.
El estado mutable se vería así:
counter = mutableVariable(0)
increaseCounter(cnt) =
setIncreasedValue(oldValue) = setValue(cnt, oldValue + 1)
composeMonad(getValue(cnt), setIncreasedValue)
printCounter(cnt) = composeMonad( getValue(cnt), print )
main = composeVoidMonad( increaseCounter(counter), printCounter(counter) )
Aquí mutableVariable
hay una función que toma valor de cualquier tipo T
y produce a MutableVariable<T>
. La función getValue
toma MutableVariable
y devuelve un IO<T>
que produce su valor actual. setValue
toma una MutableVariable<T>
y una T
y devuelve una IO<void>
que establece el valor. composeVoidMonad
es lo mismo que composeMonad
excepto que el primer argumento es un IO
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.