functional programming - programming - ¿Son los efectos secundarios una buena cosa?
functional programming react (13)
Bueno, es mucho más fácil y más intuitivo de programar con efectos secundarios, por un lado. La programación funcional es difícil para muchas personas para entenderlo: encuentre a alguien que haya enseñado o asistido a una clase en Ocaml y probablemente obtenga todo tipo de historias sobre el fracaso abyecto de la gente para comprenderlo. ¿Y de qué sirve tener un código funcional maravillosamente diseñado y maravillosamente libre de efectos secundarios si nadie puede realmente seguirlo? Hace que la contratación de personas para hacer su software sea bastante difícil.
Ese es un lado de la discusión, al menos. Hay muchas razones por las que mucha gente tendrá que aprender todo sobre el estilo funcional y el código sin efectos secundarios. El multihilo viene a la mente.
Siento el término bastante peyorativo. Por lo tanto, estoy asombrado por las dos oraciones en Wikipedia:
La programación imperativa es conocida por emplear efectos secundarios para hacer que los programas funcionen. La programación funcional a su vez es conocida por su minimización de los efectos secundarios. [1]
Ya que estoy algo sesgado en matemáticas, este último suena excelente. ¿Cuáles son los argumentos para los efectos secundarios? ¿Significan la pérdida de control o la aceptación de la incertidumbre? ¿Son algo bueno?
De vez en cuando veo una pregunta en SO que me obliga a dedicar media hora a editar un artículo realmente malo de Wikipedia. El artículo ahora es solo moderadamente malo. En la parte que se refiere a su pregunta, escribí lo siguiente:
En informática, se dice que una función o expresión tiene un efecto secundario si, además de producir un valor, también modifica algún estado o tiene una interacción observable con las funciones de llamada o el mundo exterior. Por ejemplo, una función puede modificar una variable global o estática, modificar uno de sus argumentos, generar una excepción, escribir datos en una pantalla o archivo, leer datos, llamar a otras funciones de efectos secundarios o lanzar misiles. En presencia de efectos secundarios, el comportamiento de un programa depende de la historia pasada; Es decir, el orden de evaluación importa. Debido a que entender un programa efectivo requiere pensar en todas las historias posibles, los efectos secundarios a menudo hacen que un programa sea más difícil de entender.
Los efectos secundarios son esenciales para permitir que un programa interactúe con el mundo exterior (personas, sistemas de archivos, otras computadoras en redes). Pero el grado de uso de los efectos secundarios depende del paradigma de programación. La programación imperativa es conocida por el uso incontrolado y promiscuo de los efectos secundarios. En la programación funcional, los efectos secundarios rara vez se utilizan. Los lenguajes funcionales como Standard ML y Scheme no restringen los efectos secundarios, pero es habitual que los programadores los eviten. El lenguaje funcional Haskell restringe los efectos secundarios con un sistema de tipo estático; solo una función que produce un resultado de tipo IO puede tener efectos secundarios.
El jurado aún está deliberando. Y el juicio ha continuado desde los albores de la computación, así que no esperes un veredicto pronto.
En las máquinas de von-Neumann, los efectos secundarios son cosas que hacen que la máquina funcione. Esencialmente, no importa cómo escriba su programa, tendrá que hacer efectos secundarios para que funcione (en una vista de bajo nivel).
Programar sin efectos secundarios significa abstraer los efectos secundarios para que pueda pensar en el problema en general, sin preocuparse por el estado actual de la máquina, y reducir las dependencias entre los diferentes módulos de un programa (ya sean procedimientos, clases o cualquier otra cosa). Al hacerlo, hará que su programa sea más reutilizable (ya que los módulos no dependen de un estado en particular para funcionar).
Entonces, sí, los programas libres de efectos secundarios son algo bueno, pero los efectos secundarios son inevitables en cierto nivel (por lo que no pueden considerarse "malos").
Es cierto, como mencionan algunas personas aquí, que sin efectos secundarios no se puede hacer una aplicación útil. Pero a partir de eso no se sigue que el uso de efectos secundarios de manera incontrolada sea algo bueno.
Considere la siguiente analogía: un procesador con un conjunto de instrucciones que no tenía instrucciones de bifurcación sería absolutamente inútil. Sin embargo, no se sigue que los programadores deben usar goto s todo el tiempo. Por el contrario, resultó que la programación estructurada y los lenguajes posteriores de programación orientada a objetos como Java podían prescindir incluso de tener una declaración goto, y nadie se lo perdió.
(Para estar seguro, todavía hay goto en Java, ahora se llama romper , continuar y lanzar ).
Los efectos secundarios son como cualquier otra arma. Son incuestionablemente útiles y potencialmente muy peligrosos cuando se manejan incorrectamente.
Como las armas, tienes efectos secundarios de todos los tipos diferentes de todos los diferentes grados de letalidad.
En C ++, los efectos secundarios son totalmente ilimitados, gracias a los punteros. Si una variable se declara como "privada", aún puede acceder o cambiarla utilizando trucos de puntero. Incluso puede modificar variables que no están dentro del alcance, como los parámetros y los locales de la función de llamada. Con un poco de ayuda del SO (mmap), ¡incluso puede modificar el código de máquina de su programa en tiempo de ejecución! Cuando escribes en un lenguaje como C ++, eres elevado al rango de Bit God, maestro de toda la memoria en tu proceso. Todas las optimizaciones que el compilador hace a su código se realizan asumiendo que usted no abusa de sus poderes.
En Java, tus habilidades son más restringidas. Todas las variables del alcance están bajo su control, incluidas las variables compartidas por diferentes subprocesos, pero siempre debe cumplir con el sistema de tipos. Aún así, gracias a un subconjunto del sistema operativo que está a su disposición y la existencia de campos estáticos, su código puede tener efectos no locales. Si un hilo separado cierra System.out de alguna manera, parecerá magia. Y será mágico: magia efectiva lateral.
Haskell (a pesar de la propaganda acerca de ser puro) tiene la mónada IO, que requiere que registre todos sus efectos secundarios con el sistema de tipos. Envolver su código en la mónada IO es como el período de espera de 3 días para las pistolas: todavía puede volar su propio pie, pero no hasta que lo apruebe con el gobierno. También está unsafePerformIO y su ilk, que son el mercado negro de Haskell IO, que le da efectos secundarios con "sin preguntas".
Miranda, la antecesora de Haskell, es un lenguaje funcional puro creado antes de que las mónadas se hicieran populares. Miranda (por lo que he aprendido ... si me equivoco, sustituyo a Lambda Calculus) no tiene primitivas de OI. La única IO realizada es compilar el programa (la entrada) y ejecutar el programa e imprimir el resultado (la salida). Aquí, tienes la pureza completa. El orden de ejecución es completamente irrelevante. Todos los "efectos" son locales a las funciones que los declaran, lo que significa que nunca pueden afectarse entre sí dos partes desunidas del código. Es una utopía (para los matemáticos). O equivalentemente una distpia. Es aburrido. Nunca pasa nada. No puedes escribir un servidor para ello. No se puede escribir un sistema operativo en él. No puedes escribir SNAKE o Tetris en ella. Todo el mundo simplemente se sienta alrededor mirando matemático.
Los efectos secundarios son esenciales para una parte significativa de la mayoría de las aplicaciones. Las funciones puras tienen muchas ventajas. Son más fáciles de pensar porque no tiene que preocuparse por las condiciones previas y posteriores. Dado que no cambian de estado, son más fáciles de paralelizar, lo que será muy importante a medida que aumente el número de procesadores.
Los efectos secundarios son inevitables. Y deben usarse siempre que sean una mejor opción que una solución más complicada pero pura. Lo mismo ocurre con las funciones puras. A veces, un problema se aborda mejor con una solución funcional.
Todo está bien =) Debes usar diferentes paradigmas de acuerdo con el problema que estás resolviendo.
Pro:
- Al final los efectos secundarios son lo que quieres lograr.
- Los efectos secundarios son naturales para el código que interactúa con el mundo exterior.
- Hacen muchos algoritmos simples.
- Para evitar el uso de efectos secundarios, debe implementar bucles por recursión, por lo que su implementación de lenguaje necesita una optimización de llamadas de cola.
Estafa:
- El código puro es fácil de paralelizar.
- Los efectos secundarios pueden hacer que el código sea complicado.
- El código puro es más fácil de demostrar correcto.
Por ejemplo, Haskell, al principio parece muy elegante, pero luego necesitas comenzar a jugar con el mundo exterior y ya no es tan divertido. (Haskell mueve el estado como un parámetro de función y lo oculta en cosas llamadas Mónadas, que le permiten escribir en un estilo imperativo de aspecto similar).
Sin efectos secundarios, no puede realizar operaciones de E / S; por lo que no se puede hacer una aplicación útil.
Sin efectos secundarios, simplemente no puedes hacer ciertas cosas. Un ejemplo es la E / S, ya que hacer que aparezca un mensaje en la pantalla es, por definición, un efecto secundario. Por eso es un objetivo de la programación funcional minimizar los efectos secundarios, en lugar de eliminarlos por completo.
Dejando eso de lado, a menudo hay casos en los que los efectos secundarios minimizan los conflictos con otros objetivos, como la velocidad o la eficiencia de la memoria. Otras veces, ya existe un modelo conceptual de su problema que se alinea bien con la idea de mutar el estado, y luchar contra ese modelo existente puede desperdiciar energía y esfuerzo.
Ya que su programa debe tener efectos secundarios para tener cualquier salida o efecto interesante (además de calentar su CPU), la pregunta es más bien dónde se deben activar estos efectos secundarios en su programa. Se vuelven dañinos solo si están ocultos en métodos que no los esperas.
Como regla general: separe los métodos puros y los métodos con efectos secundarios. Un método que imprime algo en la consola debe hacer solo eso y no calcular algún valor interesante que pueda querer usar en otro lugar.
Los efectos secundarios son un mal necesario , y uno debe tratar de minimizarlos / localizarlos.
Otros comentarios en el hilo dicen que la programación sin efectos a veces no es tan intuitiva , pero creo que lo que la gente considera "intuitivo" es en gran medida el resultado de su experiencia previa, y la experiencia de la mayoría de las personas tiene un fuerte sesgo imperativo. Las herramientas principales se están volviendo más y más funcionales cada día, porque la gente está descubriendo que la programación libre de efectos conduce a menos errores (aunque a veces hay una clase de errores nueva / diferente) debido a la menor posibilidad de que los componentes interactúen por medio de los efectos.
Casi nadie ha mencionado el rendimiento, y la programación sin efectos por lo general tiene un rendimiento peor que el efectivo, ya que las computadoras son máquinas von-Neumann que están diseñadas para funcionar bien con efectos (en lugar de estar diseñadas para funcionar bien con lambdas). Ahora que estamos en medio de la revolución de múltiples núcleos, esto puede cambiar el juego a medida que las personas descubren que necesitan aprovechar los núcleos para obtener un rendimiento óptimo, y mientras que la paralelización a veces requiere que un científico de cohetes logre el efecto correctamente. Puede ser fácil hacerlo bien cuando estás sin efectos.
Esa cita realmente me hizo reír. Dicho esto, encuentro que la minimización de los efectos secundarios se traduce realmente en un código que es mucho más fácil de razonar y mantener. Sin embargo, no tengo el lujo de explorar la programación funcional tanto como me gustaría.
La forma en que lo veo cuando trabajo en lenguajes orientados a objetos y procedimientos que giran en torno a los efectos secundarios es contener y aislar los efectos secundarios.
Como ejemplo básico, un videojuego tiene un efecto secundario necesario al representar gráficos en una pantalla. Sin embargo, hay dos tipos diferentes de rutas de diseño aquí con respecto a los efectos secundarios.
Uno busca minimizar y aflojar el acoplamiento haciendo que el renderizador sea muy abstracto y básicamente le diga qué renderizar. Las otras partes del sistema le dicen al renderizador qué dibujar y eso podría ser un lote de primitivas como triángulos y puntos con matrices de proyección y modelview o tal vez algo más alto como modelos abstractos, cámaras y luces y partículas. De cualquier manera, tal diseño gira en torno a muchas cosas que causan efectos secundarios externos, ya que potencialmente muchas partes de la base de código incluirán cambios en el renderizador (no importa cuán abstracto o indirecto, el efecto neto es todavía un montón de cosas en tal sistema que desencadena efectos secundarios de renderización externa).
La otra forma es contener / aislar esos efectos secundarios. En lugar de decirle al renderizador qué debe renderizar, en lugar de eso, se acopla al mundo del juego (aunque esto podría ser solo algunas abstracciones básicas y tal vez el acceso a un gráfico de escena). Ahora accede a la escena por sí sola (acceso de solo lectura) y mira a través de la escena y se da cuenta de qué renderizar utilizando más de un diseño de estilo pull. Esto lleva a un mayor acoplamiento del renderizador al mundo del juego, pero también significa que los efectos secundarios relacionados con la salida de pantalla ahora están completamente contenidos dentro del renderizador.
Este último diseño contiene o aísla los efectos secundarios, y me parece que ese tipo de diseño es mucho más fácil de mantener y mantener correcto. Todavía causa efectos secundarios, pero todos los efectos relacionados con la salida de gráficos a una pantalla están ahora completamente contenidos en el renderizador. Si hay un problema allí, usted sabe que el error estará en el código del renderizador y no el resultado de un uso indebido externo por parte de alguien y le dirá lo que debe hacer.
Debido a esto, cuando se trata de acoplar, siempre me ha parecido más conveniente maximizar los acoplamientos eferentes (salientes) en cosas que causan efectos secundarios externos y minimizar los acoplamientos aferentes (entrantes). Esto se aplica independientemente de las abstracciones. En el contexto de los efectos secundarios, una dependencia de IRenderer
sigue siendo una dependencia de un Renderer
concreto en lo que respecta a la comunicación con respecto a qué efectos secundarios van a ocurrir. La abstracción no hace ninguna diferencia en cuanto a los efectos secundarios que van a ocurrir.
El renderizador debe depender del resto del mundo para que pueda aislar completamente esos efectos secundarios de la pantalla; El resto del mundo no debería depender del renderizador. El mismo tipo de analogía para un protector de archivos. Al protector de archivos no se le debe decir qué guardar en el mundo exterior. Debería mirar el mundo que lo rodea y descubrir qué ahorrar por sí solo. Tal sería el camino de diseño que busca aislar y contener efectos secundarios; Tiende a ser más basado en el impulso que en el impulso. El resultado tiende a introducir un poco más de acoplamiento (aunque podría estar suelto) si grafica las dependencias, ya que el ahorrador podría estar relacionado con cosas que ni siquiera está interesado en guardar, o el renderizador puede necesitar acceso de solo lectura a las cosas. Ni siquiera está interesado en renderizar para descubrir las cosas que está interesado en renderizar.
Sin embargo, el resultado final es que las dependencias se alejan de los efectos secundarios en lugar de los efectos secundarios. Cuando tenemos un sistema con muchas dependencias que fluyen para impulsar efectos secundarios externos, siempre he encontrado que son los más difíciles de razonar, ya que muchas partes del sistema podrían estar cambiando de estado externo al punto en que no solo es difícil entender qué es Va a pasar pero también cuándo y dónde . Así que la forma más sencilla de corregir / prevenir ese problema es tratar de hacer que las dependencias fluyan lejos de los efectos secundarios, no hacia ellos.
De todos modos, he encontrado que favorecer a estos tipos de diseños es la forma práctica de ayudar a evitar errores y también a detectarlos y aislarlos cuando existen para que sean más fáciles de reproducir y corregir.
Otra estrategia útil que encuentro es hacer que los efectos secundarios sean más homogéneos para cualquier ciclo / fase del sistema. Por ejemplo, en lugar de hacer un bucle que elimina los datos asociados de algo, los desvincula y luego los elimina, lo encuentro mucho más fácil si haces tres bucles homogéneos en tales casos. El primer bucle homogéneo puede eliminar los datos asociados. El segundo bucle homogéneo puede desvincular el nodo. El tercer bucle homogéneo puede eliminarlo del resto del sistema. Está en una nota de nivel inferior relacionada más con la implementación que con el diseño, pero a menudo me parece que el resultado es más fácil de razonar, mantener e incluso optimizar (más fácil de paralelizar, por ejemplo, y con una localidad de referencia mejorada). esos bucles no homogéneos provocan múltiples tipos de efectos secundarios y los dividen en múltiples bucles homogéneos, cada uno de los cuales desencadena solo un tipo de efecto secundario uniforme.