¿Los cierres con efectos secundarios se consideran "estilo funcional"?

9

Muchos lenguajes de programación modernos admiten algún concepto de cierre , es decir, un fragmento de código (un bloque o una función) que

  1. Puede tratarse como un valor y, por lo tanto, almacenarse en una variable, transmitirse a diferentes partes del código, definirse en una parte de un programa e invocarse en una parte totalmente diferente del mismo programa.
  2. Puede capturar variables del contexto en el que se define y acceder a ellas cuando se invoca posteriormente (posiblemente en un contexto totalmente diferente).

Aquí hay un ejemplo de un cierre escrito en Scala:

def filterList(xs: List[Int], lowerBound: Int): List[Int] =
  xs.filter(x => x >= lowerBound)

El literal de la función x => x >= lowerBoundcontiene la variable libre lowerBound, que está cerrada (vinculada) por el argumento de la función filterListque tiene el mismo nombre. El cierre se pasa al método de la biblioteca filter, que puede invocarlo repetidamente como una función normal.

He estado leyendo muchas preguntas y respuestas en este sitio y, por lo que entiendo, el término cierre a menudo se asocia automáticamente con la programación funcional y el estilo de programación funcional.

La definición de programación de funciones en wikipedia dice:

En informática, la programación funcional es un paradigma de programación que trata la computación como la evaluación de funciones matemáticas y evita el estado y los datos mutables. Enfatiza la aplicación de funciones, en contraste con el estilo de programación imperativo, que enfatiza los cambios de estado.

y más adelante

[...] en el código funcional, el valor de salida de una función depende solo de los argumentos que se ingresan a la función [...]. Eliminar los efectos secundarios puede hacer que sea mucho más fácil comprender y predecir el comportamiento de un programa, que es una de las motivaciones clave para el desarrollo de la programación funcional.

Por otro lado, muchas construcciones de cierre proporcionadas por lenguajes de programación permiten un cierre para capturar variables no locales y cambiarlas cuando se invoca el cierre, produciendo así un efecto secundario en el entorno en el que se definieron.

En este caso, los cierres implementan la primera idea de programación funcional (las funciones son entidades de primera clase que se pueden mover como otros valores) pero descuidan la segunda idea (evitando los efectos secundarios).

¿Este uso de cierres con efectos secundarios se considera un estilo funcional o los cierres se consideran una construcción más general que puede usarse tanto para un estilo de programación funcional como no funcional? ¿Hay alguna literatura sobre este tema?

NOTA IMPORTANTE

No estoy cuestionando la utilidad de los efectos secundarios o de tener cierres con efectos secundarios. Además, no estoy interesado en una discusión sobre las ventajas / desventajas de los cierres con o sin efectos secundarios.

Solo me interesa saber si el uso de tales cierres todavía se considera estilo funcional por el proponente de la programación funcional o si, por el contrario, se desaconseja su uso cuando se usa un estilo funcional.

Giorgio
fuente
3
En un estilo / lenguaje puramente funcional, los efectos secundarios son imposibles ... así que supongo que sería una cuestión de: ¿a qué nivel de pureza se considera que el código es funcional?
Steven Evers
@SnOrfus: ¿Al 100%?
Giorgio
1
Los efectos secundarios son imposibles en un lenguaje puramente funcional, por lo que debería responder a su pregunta.
Steven Evers
@SnOrfus: En muchos idiomas, los cierres se consideran una construcción de programación funcional, por lo que estaba un poco confundido. Me parece que (1) su uso es más general que solo hacer FP, y (2) el uso de cierres no garantiza automáticamente que uno esté usando un estilo funcional.
Giorgio
¿Puede el votante dejar una nota sobre cómo mejorar esta pregunta?
Giorgio

Respuestas:

7

No; La definición de paradigma funcional es sobre la falta de estado y la falta implícita de efectos secundarios. No se trata de funciones de alto orden, cierres, manipulación de listas compatibles con el lenguaje u otras características del lenguaje ...

El nombre de la programación funcional proviene de la noción matemática de funciones : las llamadas repetidas en la misma entrada siempre dan la misma salida : función nipipotente. Esto solo se puede lograr si los datos son inmutables . Para facilitar el desarrollo, las funciones se volvieron mutables (las funciones cambian, los datos aún son inmutables) y, por lo tanto, la noción de funciones de orden superior (funciones en matemáticas, como derivadas, por ejemplo), una función que toma como entrada otra función. Para la posibilidad de llevar a cabo funciones y pasarlas como argumentos, se adoptaron funciones de primera clase ; Después de esto, para mejorar aún más la productividad, aparecieron cierres .

Esta es, por supuesto, una vista muy simplificada.

m3th0dman
fuente
3
Las funciones de orden superior no tienen absolutamente nada que ver con la mutabilidad, ni tampoco las funciones de primera clase. Puedo pasar funciones y construir otras funciones a partir de ellas en Haskell con gran facilidad, sin tener ningún estado mutable ni efectos secundarios.
tdammers
@tdammers Probablemente no expresé muy claramente; Cuando dije que las funciones se volvieron mutables, no me referí a la mutabilidad de los datos, sino al hecho de que el comportamiento de una función se puede cambiar (mediante una función de orden superior).
m3th0dman el
Una función de orden superior no tiene que cambiar una función existente; La mayoría de los ejemplos de libros de texto combinan funciones existentes en nuevas funciones sin modificarlas en absoluto: tome map, por ejemplo, qué toma una función, la aplica a una lista y devuelve una lista de los resultados. mapno modifica ninguno de sus argumentos, no cambia el comportamiento de la función que toma como argumento, pero definitivamente es una función de orden superior: si la aplica parcialmente, solo con el parámetro de función, ha construido un nueva función que opera en una lista, pero aún no ha sucedido ninguna mutación.
tdammers
@tdammers: Exactamente: la mutabilidad solo se usa en lenguajes imperativos o multi-paradigmáticos. Aunque estos pueden tener el concepto de una función (o método) de orden superior y de un cierre, abandonan la inmutabilidad. Esto puede ser útil (no digo que uno no deba hacerlo), pero mi pregunta es si aún puede llamar a esto funcional. Lo que significaría que, en general, un cierre no es un concepto estrictamente funcional.
Giorgio
@Giorgio: la mayor parte de las lenguas clásicas FP hacer que la mutabilidad; Haskell es el único que se me ocurre en la cabeza que no permite la mutabilidad en absoluto. Aún así, evitar el estado mutable es un valor importante en FP.
tdammers
9

No. "Estilo funcional" implica una programación libre de efectos secundarios.

Para ver por qué, eche un vistazo a la entrada del blog de Eric Lippert sobre el ForEach<T>método de extensión y por qué Microsoft no incluyó un método de secuencia como este en Linq :

Me opongo filosóficamente a proporcionar tal método, por dos razones.

La primera razón es que hacerlo viola los principios de programación funcional en los que se basan todos los demás operadores de secuencia. Claramente, el único propósito de una llamada a este método es causar efectos secundarios. El propósito de una expresión es calcular un valor, no causar un efecto secundario. El propósito de una declaración es causar un efecto secundario. El sitio de la llamada de esta cosa se parecería muchísimo a una expresión (aunque, es cierto, dado que el método es de retorno nulo, la expresión solo podría usarse en un contexto de "expresión de declaración"). No me sienta bien crea el único operador de secuencia que solo es útil por sus efectos secundarios.

La segunda razón es que hacerlo agrega cero poder de representación nuevo al lenguaje. Hacer esto le permite reescribir este código perfectamente claro:

foreach(Foo foo in foos){ statement involving foo; }

en este código:

foos.ForEach((Foo foo)=>{ statement involving foo; });

que usa casi exactamente los mismos caracteres en un orden ligeramente diferente. Y, sin embargo, la segunda versión es más difícil de entender, más difícil de depurar e introduce la semántica de cierre, lo que puede cambiar la vida útil de los objetos de manera sutil.

Robert Harvey
fuente
1
Aunque, estoy de acuerdo con él ... su argumento se debilita un poco al considerarlo ParallelQuery<T>.ForAll(...). La implementación de este tipo IEnumerable<T>.ForEach(...)es extremadamente útil para depurar las ForAlldeclaraciones (reemplace ForAllcon ForEachy elimine el AsParallel()y puede hacerlo / depurarlo mucho más fácilmente)
Steven Evers
Claramente, Joe Duffy tiene ideas diferentes. : D
Robert Harvey
Scala tampoco exige pureza: puede pasar un cierre no puro a una función de orden superior. Mi impresión es que la idea de un cierre no es específica de la programación funcional, pero es una idea más general.
Giorgio
55
@Giorgio: Los cierres no tienen que ser puros para seguir considerándose cierres. Sin embargo, deben ser puros para ser considerados "Estilo Funcional".
Robert Harvey
3
@Giorgio: es realmente útil poder hacer un cierre alrededor del estado mutable y los efectos secundarios y pasarlo a otra función. Es contrario a los objetivos de la programación funcional. Creo que mucha confusión proviene del elegante soporte de idiomas para que lambdas sea común en lenguajes funcionales.
GlenPeterson
0

La programación funcional lleva las funciones de primera clase al siguiente nivel conceptual con seguridad, pero declarar funciones anónimas o pasar funciones a otras funciones no es necesariamente una cosa de programación funcional. En C, todo era un número entero. Un número, un puntero a datos, un puntero a una función ... todo solo int. Puede pasar punteros de función a otras funciones, hacer listas de punteros de función ... Demonios, si trabaja en lenguaje ensamblador, las funciones son realmente solo direcciones en la memoria donde se almacenan bloques de instrucciones de máquina. Darle un nombre a una función es una sobrecarga adicional para las personas que necesitan un compilador para escribir código. Entonces las funciones eran "de primera clase" en ese sentido en un lenguaje completamente no funcional.

Si lo único que haces es calcular fórmulas matemáticas en un REPL, entonces puedes ser funcionalmente puro con tu lenguaje. Pero la mayoría de la programación empresarial tiene efectos secundarios. Perder dinero mientras se espera que se complete un programa de larga duración es un efecto secundario. Tomar cualquier acción externa: escribir en un archivo, actualizar una base de datos, registrar eventos en orden, etc. requiere un cambio de estado. Podríamos debatir si el estado realmente ha cambiado si encapsula estas acciones en envoltorios inmutables que eliminan los efectos secundarios para que su código no tenga que preocuparse por ellos. Pero es como discutir si un árbol hace ruido si cae en el bosque sin nadie allí para escucharlo. El hecho es que el árbol comenzó en posición vertical y terminó en el suelo. El estado cambia cuando se hacen las cosas, aunque solo sea para informar que las cosas se hicieron.

Por lo tanto, nos queda una escala de pureza funcional, no en blanco y negro, sino en tonos de gris. Y en esa escala, cuantos menos efectos secundarios, menos mutabilidad, mejor (más funcional).

Si necesita un efecto secundario o un estado mutable en su código funcional, intente encapsularlo del resto de su programa lo mejor que pueda. Usar un cierre (o cualquier otra cosa) para inyectar efectos secundarios o estado mutable en funciones que de otra forma serían puras es la antítesis de la programación funcional. La única excepción podría ser si el cierre fuera la forma más efectiva de encapsular los efectos secundarios del código al que se pasa. Todavía no es "programación funcional", pero podría ser el armario que puede obtener en ciertas situaciones.

GlenPeterson
fuente