La programación funcional incluye muchas técnicas diferentes. Algunas técnicas están bien con efectos secundarios. Pero un aspecto importante es el razonamiento equitativo : si invoco una función con el mismo valor, siempre obtengo el mismo resultado. Entonces puedo sustituir una llamada de función con el valor de retorno y obtener un comportamiento equivalente. Esto facilita razonar sobre el programa, especialmente al depurar.
Si la función tiene efectos secundarios, esto no se cumple. El valor de retorno no es equivalente a la llamada a la función, porque el valor de retorno no contiene los efectos secundarios.
La solución es dejar de usar secundarios efectos y la codificación de estos efectos en el valor de retorno . Diferentes idiomas tienen diferentes sistemas de efectos. Por ejemplo, Haskell usa mónadas para codificar ciertos efectos como IO o mutación de estado. Los lenguajes C / C ++ / Rust tienen un sistema de tipos que puede impedir la mutación de algunos valores.
En un lenguaje imperativo, una print("foo")
función imprimirá algo y no devolverá nada. En un lenguaje funcional puro como Haskell, una print
función también toma un objeto que representa el estado del mundo exterior y devuelve un nuevo objeto que representa el estado después de haber realizado esta salida. Algo similar a newState = print "foo" oldState
. Puedo crear tantos estados nuevos del estado anterior como quiera. Sin embargo, solo uno será utilizado por la función principal. Entonces necesito secuenciar los estados de múltiples acciones encadenando las funciones. Para imprimir foo bar
, podría decir algo como print "bar" (print "foo" originalState)
.
Si no se usa un estado de salida, Haskell no realiza las acciones que conducen a ese estado, porque es un lenguaje vago. Por el contrario, esta pereza solo es posible porque todos los efectos están codificados explícitamente como valores de retorno.
Tenga en cuenta que Haskell es el único lenguaje funcional de uso común que utiliza esta ruta. Otros lenguajes funcionales incl. la familia Lisp, la familia ML y los lenguajes funcionales más nuevos, como Scala, desalientan pero permiten efectos secundarios: podrían denominarse lenguajes funcionales imperativos.
El uso de efectos secundarios para E / S probablemente esté bien. A menudo, la E / S (que no sea el registro) solo se realiza en el límite exterior de su sistema. No se produce comunicación externa dentro de su lógica empresarial. Entonces es posible escribir el núcleo de su software en un estilo puro, sin dejar de realizar E / S impuras en una capa externa. Esto también significa que el núcleo puede ser apátrida.
La apatridia tiene una serie de ventajas prácticas, como una mayor razonabilidad y escalabilidad. Esto es muy popular para los backends de aplicaciones web. Cualquier estado se mantiene afuera, en una base de datos compartida. Esto facilita el equilibrio de carga: no tengo que pegar sesiones a un servidor específico. ¿Qué pasa si necesito más servidores? Simplemente agregue otro, porque está usando la misma base de datos. ¿Qué pasa si un servidor falla? Puedo rehacer cualquier solicitud pendiente en otro servidor. Por supuesto, todavía hay estado - en la base de datos. Pero lo hice explícito y lo extraje, y podría usar un enfoque funcional puro internamente si quisiera.
Ningún lenguaje de programación elimina los efectos secundarios. Creo que es mejor decir que los lenguajes declarativos contienen efectos secundarios, mientras que los lenguajes imperativos no. Sin embargo, no estoy tan seguro de que nada de esta charla sobre los efectos secundarios llegue a la diferencia fundamental entre los dos tipos de idiomas y que realmente parezca lo que está buscando.
Creo que ayuda ilustrar la diferencia con un ejemplo.
La línea de código anterior podría escribirse en prácticamente cualquier idioma, entonces, ¿cómo podemos determinar si estamos usando un lenguaje imperativo o declarativo? ¿En qué se diferencian las propiedades de esa línea de código en las dos clases de lenguaje?
En un lenguaje imperativo (C, Java, Javascript, etc.), esa línea de código simplemente representa un paso en un proceso. No nos dice nada sobre la naturaleza fundamental de ninguno de los valores. Nos dice que en el momento después de esta línea de código (pero antes de la siguiente línea)
a
será igual ab
más,c
pero no nos dice nada sobrea
el sentido más amplio.En un lenguaje declarativo (Haskell, Scheme, Excel, etc.) esa línea de código dice mucho más. Establece una relación invariante entre
a
y los otros dos objetos de tal manera que siempre será el casoa
igual ab
másc
. Tenga en cuenta, que incluí Excel en la lista de lenguajes declarativos porque incluso sib
oc
cambios de valor, el hecho será aún permanecen quea
será igual a su suma.En mi opinión , esto , no los efectos secundarios o el estado, es lo que hace que los dos tipos de idiomas sean diferentes. En un lenguaje imperativo, cualquier línea de código en particular no le dice nada sobre el significado general de las variables en cuestión. En otras palabras,
a = b + c
solo significa que por un breve momento en el tiempo,a
pasó a ser igual a la suma deb
yc
.Mientras tanto, en los lenguajes declarativos, cada línea de código establece una verdad fundamental que existirá durante toda la vida útil del programa. En estos idiomas,
a = b + c
le dice que pase lo que pase en cualquier otra línea de códigoa
siempre será igual a la suma deb
yc
.fuente