¿Por qué es más fácil razonar sobre lenguajes de programación y programas que no tienen efectos secundarios?

8

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 letrecse 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 letreccon 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.

ceving
fuente
Esa línea "más fácil de razonar" es pura propaganda. Siempre se da como un artículo de fe, no se necesita ni se ofrece evidencia, y cuando se analiza críticamente ni siquiera pasa la prueba de la risa. Como notó, ¡es trivialmente obvio que la versión Y Combinator es más del doble de complicada y, por lo tanto, más difícil de entender y razonar!
Mason Wheeler
55
@MasonWheeler Pasar un objeto mutable a varios métodos hace que sea difícil saber dónde se está utilizando únicamente como entrada y dónde se está mutando en su lugar. La alternativa funcional, devolver una nueva copia del objeto, lo deja claro. No voy a decir que puro siempre es mejor, pero es difícil afirmar que los gráficos grandes de objetos mutables son fáciles de razonar. Hay demasiado contexto invisible involucrado.
Doval
@Doval ¿Cómo se "aclara" cuando ahora tienes varias copias de tus objetos corriendo, algunas de las cuales son obsoletas, otras canónicas, y ahora tienes que mantener eso en orden? ¡Eso suena aún más confuso! (O, alternativamente, debe asegurarse de que no hay ninguna referencia a ningún copias secundarias, lo cual es una tarea exactamente equivalente a la gestión de memoria manual, que FP encontró taaaaaaaaaaaaaaaan confuso y difícil de razonar acerca de que inventó la recolección de basura con el fin de evitar la necesidad hacerlo!)
Mason Wheeler
2
@MasonWheeler Incluso cuando se supone que los datos deben cambiar, usted quiere tener el control de quién los está cambiando. Desea pasarlo a un método que no debe mutarlo, pero alguien podría arruinarlo e introducir un error que de todos modos termine mutando los datos. Luego terminas haciendo "copias defensivas" (¡lo cual es realmente una recomendación en el libro Effective Java!) Y haciendo más trabajo / generando más basura que usando una estructura de datos inmutable desde el principio. El hecho de que los datos van a cambiar nunca se interpuso en el camino de que alguien use cadenas o tipos numéricos inmutables.
Doval
2
Los lenguajes @MasonWheeler FP no generan mucha basura, de lo contrario serían inútiles. Así no es como funcionan "detrás de escena". El "más fácil de razonar" generalmente se refiere al razonamiento equitativo, que no es cosa de risa. El razonamiento equitativo se puede hacer en muchos paradigmas, con un éxito variable, pero en los lenguajes FP generalmente es más fácil, y eso es una gran victoria (aunque a costa de otras cosas; todo es una compensación en la vida).
Andres F.

Respuestas:

13

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.

Karl Bielefeldt
fuente
10

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).

Jörg W Mittag
fuente
también podría llegar a un punto muerto o un bloqueo en vivo, no es que tampoco sea mejor ...
Deduplicador
6

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.

Erik Eidt
fuente
Tenga en cuenta que el aliasing sólo es posible con las dos variables de mutables y referencias. Un idioma con solo uno u otro no tiene este problema
gardenhead
4

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.

Cort Ammon
fuente
1

"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 que fes 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:

let f = \ n -> if n < 2 
                 then 1 
                 else n*(f (n-1)) 
        in (f 5)

(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

Periata Breatta
fuente
En general es un efecto secundario, porque letpodría devolver la función local.
ceving
2
@ceving - incluso entonces, no es un efecto secundario, porque la modificación de la ubicación de almacenamiento está limitada en el tiempo que puede ocurrir en un tiempo antes de que cualquier otro código pueda leerlo . Para que un efecto secundario sea real, debe ser posible que algún agente externo se dé cuenta de que ha sucedido; en este caso, no hay forma posible de que eso suceda.
Periata Breatta
0

Esto no se explica más. Intento encontrar una explicación.

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 forvariable 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