Pregunta

Siento que los efectos secundarios son un fenómeno natural.Pero es algo así como un tabú en los lenguajes funcionales.¿Cuales son las razones?

Mi pregunta es específica del estilo de programación funcional.No todos los lenguajes/paradigmas de programación.

¿Fue útil?

Solución

Escribir sus funciones/métodos sin efectos secundarios, por lo que están funciones puras - Hace que sea más fácil razonar sobre la exactitud de su programa.

También facilita la composición de esas funciones para crear un nuevo comportamiento.

También hace posible ciertas optimizaciones, donde el compilador puede, por ejemplo, memorarse los resultados de las funciones o usar la eliminación de la subexpresión común.

Editar: a solicitud de Benjol: porque gran parte de su estado está almacenado en la pila (flujo de datos, no flujo de control, como Jonas lo ha llamado aquí), puede paralelear o reordenar la ejecución de aquellas partes de su cálculo que son independientes entre sí. Puede encontrar fácilmente esas partes independientes porque una parte no proporciona entradas al otro.

En entornos con depuradores que le permiten revertir la pila y la computación de reanudar (como SmallTalk), tener funciones puras significa que puede ver muy fácilmente cómo cambia un valor, porque los estados anteriores están disponibles para su inspección. En un cálculo de mutación pesada, a menos que agregue explícitamente las acciones de DO/deshacer a su estructura o algoritmo, no puede ver el historial del cálculo. (Esto vuelve al primer párrafo: escribir funciones puras hace que sea más fácil inspeccionar la corrección de su programa).

Otros consejos

De un artículo sobre Programación funcional:

En la práctica, las aplicaciones deben tener algunos efectos secundarios. Simon Peyton-Jones, un importante contribuyente al lenguaje de programación funcional Haskell, dijo lo siguiente: "Al final, cualquier programa debe manipular el estado. Un programa que no tiene ningún efecto secundario es un tipo de caja negra. Todo lo que se puede decir es que es decir es que la caja se calienta más ". (http://oscon.blip.tv/file/324976) La clave es limitar los efectos secundarios, identificarlos claramente y evitar dispersarlos por todo el código.

Lo has equivocado, la programación funcional promueve los efectos secundarios limitantes para hacer que los programas sean fáciles de entender y optimizar. Incluso Haskell le permite escribir en archivos.

Esencialmente, lo que digo es que los programadores funcionales no creen que los efectos secundarios sean malos, simplemente piensan que limitar el uso de efectos secundarios es bueno. Sé que puede parecer una distinción tan simple, pero marca la diferencia.

Algunas notas:

  • Las funciones sin efectos secundarios pueden ejecutarse trivially en paralelo, mientras que las funciones con efectos secundarios generalmente requieren algún tipo de sincronización.

  • Las funciones sin efectos secundarios permiten una optimización más agresiva (por ejemplo, mediante el uso de una memoria caché de resultados), porque mientras obtengamos el resultado correcto, ni siquiera importa si la función fue o no. De Verdad ejecutado

Principalmente trabajo en código funcional ahora, y desde esa perspectiva parece cegadoramente obvio. Efectos secundarios Crear un enorme Carga mental para los programadores que intentan leer y comprender el código. No notas esa carga hasta que estés libre de ella por un tiempo, y de repente tienes que leer el código con efectos secundarios nuevamente.

Considere este simple ejemplo:

val foo = 42
// Several lines of code you don't really care about, but that contain a
// lot of function calls that use foo and may or may not change its value
// by side effect.

// Code you are troubleshooting
// What's the expected value of foo here?

En un lenguaje funcional, yo saber que foo sigue siendo 42. Ni siquiera tengo que Mira En el código intermedio, mucho menos entendiéndolo, o mire las implementaciones de las funciones que llama.

Todo eso sobre la concurrencia y la paralelización y la optimización son agradables, pero eso es lo que los científicos informáticos ponen en el folleto. No tener que preguntarse quién está mutando tu variable y cuándo es lo que realmente disfruto en la práctica diaria.

Pocos o ningún idioma hacen que sea imposible causar efectos secundarios. Los idiomas que estaban completamente libres de efectos secundarios serían prohibitivamente difíciles (casi imposibles) de usar, excepto en una capacidad muy limitada.

¿Por qué los efectos secundarios se consideran malvados?

Porque hacen que sea mucho más difícil razonar exactamente lo que hace un programa y demostrar que hace lo que espera que haga.

En un nivel muy alto, imagine probar un sitio web completo de 3 niveles con solo pruebas de caja negra. Claro, es factible, dependiendo de la escala. Pero ciertamente hay mucha duplicación. Y si hay es Un error (que está relacionado con un efecto secundario), entonces podría romper todo el sistema para obtener más pruebas, hasta que el error sea diagnosticado y reparado, y la solución se implementa en el entorno de prueba.

Beneficios

Ahora, escévelo. Si fuera bastante bueno escribiendo código libre de efectos secundarios, ¿cuánto más rápido sería razonando lo que hizo algún código existente? ¿Cuánto más rápido podría escribir pruebas unitarias? ¿Qué tan seguro sería que el código sin efectos secundarios estaba libre de errores, y que los usuarios podrían limitar su exposición a cualquier error? hizo ¿tener?

Si el código no tiene efectos secundarios, el compilador también puede tener optimizaciones adicionales que podría realizar. Puede ser mucho más fácil implementar esas optimizaciones. Puede ser mucho más fácil incluso conceptualizar una optimización para el código libre de efectos secundarios, lo que significa que su proveedor de compiladores podría implementar optimizaciones que son difíciles de imposibles en el código con los efectos secundarios.

La concurrencia también es drásticamente más simple de implementar, generar automáticamente y optimizar cuándo el código no tiene efectos secundarios. Esto se debe a que todas las piezas se pueden evaluar de manera segura en cualquier orden. Permitir que los programadores escriban un código altamente concurrente se considera ampliamente el próximo gran desafío que la informática necesita abordar, y una de las pocas setos restantes en contra Ley de Moore.

Los efectos secundarios son como "fugas" en su código que deberá ser manejado más tarde, ya sea por usted o un compañero de trabajo desprevenido.

Los lenguajes funcionales evitan las variables de estado y los datos mutables como una forma de hacer que el código sea menos dependiente del contexto y más modular. La modularidad asegura que el trabajo de un desarrollador no afectará/socavará el trabajo de otro.

La tasa de desarrollo de desarrollo con el tamaño del equipo es un "santo grial" del desarrollo de software hoy. Cuando trabajan con otros programadores, pocas cosas son tan importantes como la modularidad. Incluso los efectos secundarios lógicos más simples hacen que la colaboración sea extremadamente difícil.

Bueno, en mi humilde opinión, esto es bastante hipócrita. A nadie le gustan los efectos secundarios, pero todos los necesitan.

Lo que es tan peligroso sobre los efectos secundarios es que si llama una función, entonces esto posiblemente tiene un efecto no solo en la forma en que se comporta la función cuando se llama la próxima vez, sino que posiblemente tiene este efecto en otras funciones. Por lo tanto, los efectos secundarios introducen comportamientos impredecibles y dependencias no triviales.

Los paradigmas de programación como OO y funcionales abordan este problema. OO reduce el problema imponiendo una separación de preocupaciones. Esto significa que el estado de aplicación, que consiste en muchos datos mutables, está encapsulado en objetos, cada uno de los cuales es responsable de mantener solo su propio estado. De esta manera, se reduce el riesgo de dependencias y los problemas están mucho más aislados y más fáciles de rastrear.

La programación funcional adopta un enfoque mucho más radical, donde el estado de la aplicación es simplemente inmutable desde la perspectiva del programador. Esta es una buena idea, pero hace que el lenguaje sea inútil por sí solo. ¿Por qué? Porque cualquier operación de E/O tiene efectos secundarios. Tan pronto como lea en cualquier flujo de entrada, es probable que su estado de aplicación cambie, porque la próxima vez que invoca la misma función, es probable que el resultado sea diferente. Puede estar leyendo diferentes datos o, también una posibilidad, la operación puede fallar. Lo mismo es cierto para la salida. Incluso la salida es una operación con efectos secundarios. Esto no es nada que se diga a menudo hoy en día, pero imagine que solo tiene 20k para su salida y si sale más, su aplicación se bloquea porque está fuera de espacio en disco o lo que sea.

Entonces, sí, los efectos secundarios son desagradables y peligrosos desde la perspectiva de un programador. La mayoría de los errores provienen de la forma en que ciertas partes del estado de la aplicación están entrelazadas de una manera casi oscura, a través de efectos secundarios innecesarios no considerados y a menudo. Desde la perspectiva de un usuario, los efectos secundarios son el punto de usar una computadora. No les importa lo que sucede dentro o cómo se organiza. Hacen algo y esperan que la computadora cambie en consecuencia.

Cualquier efecto secundario introduce parámetros de entrada/salida adicionales que deben tomarse en la cuenta al probar.

Esto hace que la validación del código sea mucho más compleja, ya que el entorno no puede limitarse solo al código que se valida, pero debe traer parte o todos los entornos circundantes (el global que se actualiza en ese código, que a su vez depende de eso. El código, que a su vez depende de vivir dentro de un servidor Java EE completo ...)

Al tratar de evitar los efectos secundarios, limita la cantidad de externalismo necesario para ejecutar el código.

En mi experiencia, un buen diseño en la programación orientada a objetos exige el uso de funciones que tienen efectos secundarios.

Por ejemplo, tome una aplicación básica de escritorio de interfaz de usuario. Es posible que tenga un programa en ejecución que tenga en su gráfico de objetos en su montón que representa el estado de dominio actual de mi programa. Los mensajes llegan a los objetos en ese gráfico (por ejemplo, a través de métodos llamadas invocadas desde el controlador de capa de interfaz de usuario). El gráfico de objetos (modelo de dominio) en el montón se modifica en respuesta a los mensajes. Los observadores del modelo están informados de cualquier cambio, la interfaz de usuario y tal vez otros recursos se modifican.

Lejos de ser malvado, la disposición correcta de estos efectos secundarios modificadores y modificadores de la pantalla está en el núcleo del diseño OO (en este caso, el patrón MVC).

Por supuesto, eso no significa que sus métodos deberían tener efectos secundarios arbitrosos. Y las funciones gratuitas de efectos secundarios tienen un lugar para mejorar la lectura y, a veces, el rendimiento de su código.

El mal es un poco exagerado. Todo depende del contexto del uso del lenguaje.

Otra consideración para los ya mencionados es que hace pruebas de corrección de un programa mucho más simple si no hay efectos secundarios funcionales.

Como han señalado las preguntas anteriores, los idiomas funcionales no tanto prevenir El código de tener efectos secundarios como nos proporciona herramientas para administrar qué efectos secundarios pueden suceder en un código determinado y cuándo.

Esto resulta tener consecuencias muy interesantes. Primero, y más obviamente, hay numerosas cosas que puede hacer con el código libre de efectos secundarios, que ya se han descrito. Pero también hay otras cosas que podemos hacer, incluso cuando trabajamos con código que tiene efectos secundarios:

  • En el código con estado mutable, podemos administrar el alcance del estado de tal manera que garantizará estáticamente que no puede filtrarse fuera de una función determinada, lo que nos permite recolectar basura sin el conteo de referencia o los esquemas de estilo de marcado y barrido , aún así, asegúrese de que no hay referencias sobrevivir. Las mismas garantías también son útiles para mantener información sensible a la privacidad, etc. (esto se puede lograr utilizando la Mónada ST en Haskell)
  • Al modificar el estado compartido en múltiples hilos, podemos evitar la necesidad de bloqueos al rastrear los cambios y realizar una actualización atómica al final de una transacción, o rodar la transacción hacia atrás y repetirlo si otro hilo hizo una modificación conflictiva. Esto solo se puede lograr porque podemos asegurar que el código no tenga más efectos que las modificaciones estatales (que podemos abandonar felizmente). Esto es realizado por la monad STM (memoria transaccional de software) en Haskell.
  • Podemos rastrear los efectos del código y el cuadro de arena trivialmente, filtrando cualquier efecto que deba realizar para asegurarse de que sea seguro, lo que permite (por ejemplo) El código de entrada del usuario se ejecutará de forma segura en un sitio web

En las bases de código complejas, las interacciones complejas de los efectos secundarios son lo más difícil que me parece. Solo puedo hablar personalmente dado la forma en que funciona mi cerebro. Los efectos secundarios y los estados persistentes y las entradas mutantes, etc., me hacen pensar en "cuándo" y "donde" las cosas razonan sobre la corrección, no solo "lo que" está sucediendo en cada función individual.

No puedo concentrarme en "qué". No puedo concluir después de probar a fondo una función que causa efectos secundarios de que propagará un aire de confiabilidad a lo largo del código que lo usa, ya que las personas que llaman aún podrían mal uso que la llaman en el momento equivocado, desde el hilo incorrecto, en el incorrecto. ordenar. Mientras tanto, una función que no causa efectos secundarios y solo devuelve una nueva salida dada una entrada (sin tocar la entrada) es bastante imposible de usar mal de esta manera.

Pero creo que soy un tipo pragmático, o al menos trato de serlo, y no creo que necesariamente tengamos que eliminar todos los efectos secundarios al mínimo más mínimo para razonar sobre la corrección de nuestro código (al menos Encontraría esto muy difícil de hacer en idiomas como C). Donde me resulta muy difícil razonar sobre la corrección es cuando tenemos la combinación de flujos de control complejos y efectos secundarios.

El control complejo fluye para mí son los que son de naturaleza gráfica, a menudo recursiva o como las colas de eventos, por ejemplo, que no llaman directamente eventos de forma recursiva pero son "recursivas" en la naturaleza), tal vez haciendo las cosas En el proceso de atravesar una estructura de gráficos vinculado real, o procesar una cola de eventos no homogénea que contiene una mezcla ecléctica de eventos para procesarnos que nos lleva a todo tipo de partes diferentes de la base de código y todas las actividades de diferentes efectos secundarios. Si intentara dibujar todos los lugares que finalmente terminará en el código, se parecería a un gráfico complejo y potencialmente con los nodos en el gráfico que nunca esperaba habría estado allí en ese momento dado, y dado que todos son causando efectos secundarios, eso significa que no solo te sorprenderá qué funciones se llaman, sino también qué efectos secundarios están ocurriendo durante ese tiempo y el orden en que están ocurriendo.

Los lenguajes funcionales pueden tener flujos de control extremadamente complejos y recursivos, pero el resultado es muy fácil de comprender en términos de corrección porque no hay todo tipo de efectos secundarios eclécticos en el proceso. Solo cuando los flujos de control complejos se encuentran con efectos secundarios eclécticos que me parece que induce dolor de cabeza para tratar de comprender la totalidad de lo que está sucediendo y si siempre hará lo correcto.

Entonces, cuando tengo esos casos, a menudo me resulta muy difícil, si no imposible, sentirme muy seguro de la corrección de dicho código, y mucho menos seguro de que puedo hacer cambios en dicho código sin tropezar con algo inesperado. Por lo tanto, la solución para mí está simplificando el flujo de control o minimiza/unifica los efectos secundarios (al unificar, me refiero a solo causar un tipo de efecto secundario a muchas cosas durante una fase particular en el sistema, no dos o tres o un docena). Necesito que sucedan una de esas dos cosas para permitir que mi cerebro de Simpleton se sienta seguro de la corrección del código que existe y la corrección de los cambios que introduzco. Es bastante fácil estar seguro de la corrección del código que introduce los efectos secundarios si los efectos secundarios son uniformes y simples junto con el flujo de control, así: así:

for each pixel in an image:
    make it red

Es bastante fácil razonar sobre la corrección de dicho código, pero principalmente porque los efectos secundarios son tan uniformes y el flujo de control es muy simple. Pero digamos que teníamos código como este:

for each vertex to remove in a mesh:
     start removing vertex from connected edges():
         start removing connected edges from connected faces():
             rebuild connected faces excluding edges to remove():
                  if face has less than 3 edges:
                       remove face
             remove edge
         remove vertex

Entonces, esto es un pseudocódigo ridículamente simplificado que generalmente implicaría muchas más funciones y bucles anidados y muchas más cosas que tendrían que continuar (actualizar múltiples mapas de textura, pesas óseas, estados de selección, etc.), pero incluso el pseudocódigo hace que sea tan difícil de Razón sobre la corrección debido a la interacción del complejo flujo de control gráfico y los efectos secundarios. Entonces, una estrategia para simplificar eso es aplazar el procesamiento y centrarse en un tipo de efecto secundario a la vez:

for each vertex to remove:
     mark connected edges
for each marked edge:
     mark connected faces
for each marked face:
     remove marked edges from face
     if num_edges < 3:
          remove face

for each marked edge:
     remove edge
for each vertex to remove:
     remove vertex

... Algo a este sentido como una iteración de simplificación. Eso significa que estamos pasando a través de los datos varias veces, lo que definitivamente está incurriendo en un costo computacional, pero a menudo encontramos que podemos ver múltiples código resultante más fácilmente, ahora que los efectos secundarios y los flujos de control han tomado esta naturaleza uniforme y más simple. Además, cada bucle se puede hacer más amigable con el caché que atravesar el gráfico conectado y causar efectos secundarios a medida que avanzamos (Ej: Use un conjunto de bits paralelo para marcar lo que debe atravesar para que podamos hacer los pases diferidos en orden de secuencia ordenado Usando Bitmasks y FFS). Pero lo más importante, encuentro que la segunda versión es mucho más fácil de razonar en términos de corrección y cambio sin causar errores. Así es como lo aborro de todos modos y aplico el mismo tipo de mentalidad para simplificar el procesamiento de malla allí arriba como el manejo de eventos simplificadores, etc., más bucles homogéneos con flujos de control simples muertos que causan efectos secundarios uniformes.

Y después de todo, necesitamos que ocurran efectos secundarios en algún momento, o de lo contrario, solo tendríamos funciones que producen datos sin ningún lugar a donde ir. A menudo necesitamos grabar algo en un archivo, mostrar algo en una pantalla, enviar los datos a través de un socket, algo de este tipo, y todas estas cosas son efectos secundarios. Pero definitivamente podemos reducir la cantidad de efectos secundarios superfluos que se realizan, y también reducir la cantidad de efectos secundarios que se realizan cuando los flujos de control son muy complicados, y creo que sería mucho más fácil evitar errores si lo hiciéramos.

No es malo.En mi opinión, es necesario distinguir entre dos tipos de funciones: con y sin efectos secundarios.La función sin efectos secundarios:- devuelve siempre lo mismo con los mismos argumentos, por lo que, por ejemplo, una función sin argumentos no tiene sentido.- Eso también significa que el orden en lo que se llaman algunas funciones no juegan ningún papel, debe ser capaz de ejecutar y puede ser depurado solo (!), Sin ningún otro código.Y ahora, jajaja, mira lo que hace JUnit.Una función con efectos secundarios:- tiene una especie de "fugas", que se pueden resaltar automáticamente - es muy importante mediante la depuración y la búsqueda de errores, que generalmente son causados ​​por efectos secundarios.- Cualquier función con efectos secundarios tiene también una "parte" de sí misma sin efectos secundarios, que también se puede separar automáticamente.Tan malos son esos efectos secundarios que producen errores difíciles de rastrear.

Licenciado bajo: CC-BY-SA con atribución
scroll top