Leí "El por qué de Y" de Richard P. Gabriel . Es un artículo fácil de leer sobre el combinador Y, que es bastante raro. El artículo comienza con la definición recursiva de la función factorial:
(letrec ((f (lambda (n)
(if (< n 2) 1 (* n (f (- n 1)))))))
(f 10))
Y explica que letrec
se puede definir con un efecto secundario:
(let ((f #f))
(set! f (lambda (n)
(if (< n 2) 1 (* n (f (- n 1))))))
(f 10))
Y el resto del artículo describe que también es posible definir letrec
con el combinador Y:
(define (Y f)
(let ((g (lambda (h)
(lambda (x)
((f (h h)) x)))))
(g g)))
(let ((f (Y (lambda (fact)
(lambda (n)
(if (< n 2) 1 (* n (fact (- n 1)))))))))
(f 10))
Obviamente, esto es mucho más complicado que la versión con efectos secundarios. La razón por la cual es beneficioso preferir el combinador Y sobre el efecto secundario viene dada por la declaración:
Es más fácil razonar sobre los lenguajes de programación y los programas que no tienen efectos secundarios.
Esto no se explica más. Intento encontrar una explicación.
optimization
side-effect
ceving
fuente
fuente
Respuestas:
Obviamente, puede encontrar ejemplos de funciones puras increíblemente difíciles de leer que realizan los mismos cálculos que las funciones con efectos secundarios que son mucho más fáciles de leer. Especialmente cuando usa una transformación mecánica como un combinador en Y para llegar a una solución. Eso no es lo que se entiende por "más fácil de razonar".
La razón por la que es más fácil razonar sobre funciones sin efectos secundarios es que solo tiene que preocuparse por las entradas y salidas. Con los efectos secundarios, también debe preocuparse por cuántas veces se llaman las funciones, en qué orden se llaman, qué datos se crean dentro de la función, qué datos se comparten y qué datos se copian. Y toda esa información para cualquier función que pueda llamarse interna a la función que está llamando, y recursivamente interna a esas funciones, y así sucesivamente.
Este efecto es mucho más fácil de ver en el código de producción con varias capas que en las funciones de ejemplo de juguete. Principalmente significa que puede confiar mucho más en la firma de tipo de una función. Realmente notas la carga de los efectos secundarios si haces una programación funcional pura por un tiempo y luego vuelves a ella.
fuente
Una propiedad interesante de los lenguajes sin efectos secundarios es que la introducción de paralelismo, concurrencia o asincronía no puede cambiar el significado del programa. Puede hacerlo más rápido. O puede hacerlo más lento. Pero no puede equivocarse.
Esto hace que sea trivial paralelizar automáticamente los programas. ¡Tan trivial, de hecho, que generalmente terminas con demasiado paralelismo! El equipo de GHC experimentó con la paralelización automática. Descubrieron que incluso los programas simples podrían descomponerse en cientos, incluso miles de hilos. La sobrecarga de todos esos hilos abrumará cualquier aceleración potencial en varios órdenes de magnitud.
Entonces, para la paralelización automática de programas funcionales, el problema se convierte en "cómo agrupar pequeñas operaciones atómicas en tamaños útiles de piezas paralelas", a diferencia de los programas impuros, donde el problema es "cómo dividir las grandes operaciones monolíticas en útiles tamaños de piezas paralelas ". Lo bueno de esto es que lo primero se puede hacer de forma heurística (recuerde: si se equivoca, lo peor que puede suceder es que el programa se ejecute un poco más lento de lo que podría ser), mientras que lo último es equivalente a resolver la Detención. Problema (en el caso general), y si te equivocas, tu programa simplemente se bloqueará (¡si tienes suerte!) O devolverá resultados sutilmente incorrectos (en el peor de los casos).
fuente
Los idiomas con efectos secundarios emplean análisis de alias para ver si una ubicación de memoria posiblemente deba volver a cargarse después de una llamada a la función. Cuán conservador es este análisis depende del idioma.
Para C, esto tiene que ser bastante conservador, ya que el lenguaje no es de tipo seguro.
Para Java y C #, estos no tienen que ser tan conservadores debido a su mayor seguridad de tipos.
Ser demasiado conservador evita las optimizaciones de carga.
Tal análisis sería innecesario (o trivial dependiendo de cómo lo mire) en un lenguaje sin efectos secundarios.
fuente
Siempre hay optimizaciones para aprovechar cualquier suposición que des. Reordenar operaciones viene a la mente.
Un ejemplo divertido que viene a la mente en realidad aparece en algunos lenguajes ensambladores más antiguos. En particular, MIPS tenía una regla según la cual se ejecutaba la instrucción después de un salto condicional, independientemente de qué rama se tomara. Si no quieres esto, pones un NOP después del salto. Esto se hizo debido a la forma en que se estructuraba la tubería MIPS. Hubo un bloqueo natural de 1 ciclo integrado en la ejecución del salto condicional, por lo que también podrías hacer algo útil con ese ciclo.
Los compiladores suelen buscar una operación que deba realizarse en ambas ramas y deslizarla en esa ranura para obtener un poco más de rendimiento. Sin embargo, si un compilador no puede hacer eso, pero puede mostrar que no hubo efectos secundarios en la operación, el compilador podría pegarlo de manera oportunista en ese lugar. Por lo tanto, en una ruta, el código ejecutaría una instrucción más rápido. En el otro camino, no hay daño hecho.
fuente
"Letrec se puede definir con un efecto secundario ..." No veo ningún efecto secundario en su definición. Sí, utiliza
set!
cuál es una forma típica de producir efectos secundarios en Scheme, pero en este caso no hay efectos secundarios , ya quef
es puramente local, no puede ser referenciada por ninguna función que no sea local. Por lo tanto, no es un efecto secundario como se ve desde un alcance externo. Lo que este código hace hacer es cortar alrededor una limitación en el alcance del Esquema que por defecto no permite una definición lambda se refiere a sí mismo.Algunos otros lenguajes tienen declaraciones donde una expresión utilizada para producir el valor de una constante puede referirse a la constante misma. En dicho lenguaje, se puede usar la definición equivalente exacta, pero claramente esto no produce un efecto secundario, ya que solo se usa una constante. Ver, por ejemplo, este programa equivalente de Haskell:
(que se evalúa en 120).
Esto claramente no tiene efectos secundarios (como para que una función en Haskell tenga un efecto secundario, debe devolver su resultado envuelto en una Mónada, pero el tipo devuelto aquí es un tipo numérico simple), pero es un código estructuralmente idéntico al código que cita
fuente
let
podría devolver la función local.Es algo inherente a muchos de nosotros que hemos depurado bases de datos masivas, pero hay que lidiar con una escala lo suficientemente grande en el nivel de supervisor durante un tiempo lo suficientemente largo como para apreciarlo. Es como entender la importancia de estar en posición en el póker. Inicialmente no parece una ventaja tan útil ir al final de cada turno hasta que registre un historial de manos de un millón de manos y se dé cuenta de que ganó mucho más dinero en posición que fuera.
Dicho esto, no estoy de acuerdo con la idea de que un cambio a una variable local es un efecto secundario. Desde mi punto de vista, una función no causa efectos secundarios si no modifica nada fuera de su alcance, que cualquier cosa que toque y manipule no afectará nada debajo de la pila de llamadas o cualquier memoria o recurso que la función no adquirió .
En general, lo más difícil de razonar en una base de código compleja y a gran escala que no tiene el procedimiento de prueba más perfecto imaginable es la administración de estado persistente, como todos los cambios en objetos granulares en un mundo de videojuegos a medida que avanzas decenas de miles de funciones al intentar reducir entre una lista interminable de sospechosos cuál realmente causó la violación de un invariante en todo el sistema ("esto nunca debería suceder, ¿quién lo hizo?"). Si nunca se cambia nada fuera de una función, no puede causar un mal funcionamiento central.
Por supuesto, esto no es posible en todos los casos. Cualquier aplicación que actualice una base de datos almacenada en una máquina diferente está, por naturaleza, diseñada para causar efectos secundarios, así como cualquier aplicación diseñada para cargar y escribir archivos. Pero hay muchas más cosas que podemos estar haciendo sin efectos secundarios en muchas funciones y muchos programas si, por ejemplo, tuviéramos una rica biblioteca de estructuras de datos inmutables y adoptaramos esta mentalidad aún más.
Curiosamente, John Carmack se ha subido al carro de la LISP y la inmutabilidad a pesar de comenzar en los días de la codificación C más microajustada. Me he encontrado haciendo algo similar, aunque todavía uso mucho C. Tal es la naturaleza de los pragmáticos, creo, que han pasado años depurando y tratando de razonar sobre sistemas complejos a gran escala en su conjunto desde un nivel superior. Incluso los que son sorprendentemente robustos y desprovistos en gran medida de errores aún pueden dejarlo con la incómoda sensación de que algo mal está al acecho a la vuelta de la esquina si se modifica un estado persistente complejo entre el gráfico interconectado más complejo de llamadas de función entre Los millones de líneas de código. Incluso si cada interfaz se prueba con una prueba unitaria y todas pasan, hay '
En la práctica, a menudo encuentro que la programación funcional hace que sea más difícil comprender una función. Todavía hace girar mi cerebro en giros y nudos, especialmente con una lógica recursiva compleja. Pero toda la sobrecarga intelectual asociada con la determinación de un par de funciones escritas en un lenguaje funcional es eclipsada por la de un sistema complejo con estados persistentes que se cambian en decenas de miles de funciones, donde cada función que causa efectos secundarios se suma al total Complejidad del razonamiento sobre la corrección de todo el sistema en su conjunto.
Tenga en cuenta que no necesita un lenguaje funcional puro para que las funciones eviten los efectos secundarios. Los estados locales modificados en una función no cuentan como un efecto secundario, como una
for
variable de contador de bucle local a una función no cuenta como un efecto secundario. Incluso escribo código C hoy en día con el objetivo de evitar los efectos secundarios cuando sea posible y he ideado una biblioteca de estructuras de datos inmutables y seguras para subprocesos que pueden modificarse parcialmente mientras el resto de los datos se copia superficialmente, y me ha ayudado a mucho para razonar sobre la corrección de mi sistema. Estoy totalmente en desacuerdo con el autor en ese sentido. Al menos en C y C ++ en el software de misión crítica, una función puede documentarse como que no tiene efectos secundarios si no toca nada que pueda afectar al mundo fuera de la función.fuente