Efectos secundarios que rompen la transparencia referencial

11

La programación funcional en Scala explica el impacto de un efecto secundario en la ruptura de la transparencia referencial:

efecto secundario, lo que implica alguna violación de la transparencia referencial.

He leído parte de SICP , que analiza el uso del "modelo de sustitución" para evaluar un programa.

Como entiendo a grandes rasgos el modelo de sustitución con transparencia referencial (RT), puede descomponer una función en sus partes más simples. Si la expresión es RT, puede descomponer la expresión y obtener siempre el mismo resultado.

Sin embargo, como dice la cita anterior, el uso de efectos secundarios puede / romperá el modelo de sustitución.

Ejemplo:

val x = foo(50) + bar(10)

Si fooy bar no tiene efectos secundarios, la ejecución de cualquiera de las funciones siempre devolverá el mismo resultado x. Pero, si tienen efectos secundarios, alterarán una variable que interrumpe / arroja una llave en el modelo de sustitución.

Me siento cómodo con esta explicación, pero no la entiendo completamente.

Por favor, corríjame y complete los agujeros con respecto a los efectos secundarios que rompen la RT, discutiendo también los efectos en el modelo de sustitución.

Kevin Meredith
fuente

Respuestas:

20

Comencemos con una definición de transparencia referencial :

Se dice que una expresión es referencialmente transparente si se puede reemplazar por su valor sin cambiar el comportamiento de un programa (en otras palabras, producir un programa que tenga los mismos efectos y resultados en la misma entrada).

Lo que eso significa es que (por ejemplo) puede reemplazar 2 + 5 con 7 en cualquier parte del programa, y ​​el programa aún debería funcionar. Este proceso se llama sustitución. La sustitución es válida si, y solo si, 2 + 5 se pueden reemplazar con 7 sin afectar ninguna otra parte del programa .

Digamos que tengo una clase llamada Baz, con las funciones Fooy Baren ella. Para simplificar, solo diremos eso Fooy Barambos devolverán el valor que se pasa. Entonces Foo(2) + Bar(5) == 7, como era de esperar. La transparencia referencial garantiza que puede reemplazar la expresión Foo(2) + Bar(5)con la expresión 7en cualquier parte de su programa, y ​​el programa seguirá funcionando de manera idéntica.

Pero, ¿qué Foopasa si devuelve el valor pasado, pero Bardevuelve el valor pasado, más el último valor proporcionado Foo? Eso es bastante fácil de hacer si almacena el valor de Fooen una variable local dentro de la Bazclase. Bueno, si el valor inicial de esa variable local es 0, la expresión Foo(2) + Bar(5)devolverá el valor esperado de 7la primera vez que la invoque, pero devolverá 9la segunda vez que la invoque.

Esto viola la transparencia referencial de dos maneras. Primero, no se puede contar con Bar para que devuelva la misma expresión cada vez que se llama. En segundo lugar, se ha producido un efecto secundario, a saber, que llamar a Foo influye en el valor de retorno de Bar. Como ya no puede garantizar que Foo(2) + Bar(5)sea ​​igual a 7, ya no puede sustituirlo.

Esto es lo que significa Transparencia referencial en la práctica; una función referencialmente transparente acepta algún valor y devuelve algún valor correspondiente, sin afectar a otro código en otra parte del programa, y ​​siempre devuelve la misma salida dada la misma entrada.

Robert Harvey
fuente
55
Entonces, romper le RTimpide usar el. substitution model.El gran problema de no poder usar substitution modeles el poder de usarlo para razonar sobre un programa.
Kevin Meredith
Eso es exactamente correcto.
Robert Harvey
1
+1 respuesta maravillosamente clara y comprensible. Gracias.
Racheet
2
Además, si esas funciones son transparentes o "puras", el orden en el que realmente se ejecutan no es importante, no nos importa si foo () o bar () se ejecutan primero y, en algunos casos, es posible que nunca evalúen si no son necesarias
Zachary K
1
Otra ventaja de RT es que las caras expresiones referencialmente transparentes pueden almacenarse en caché (ya que evaluarlas una o dos veces debería producir exactamente el mismo resultado).
Dcastro
3

Imagine que está tratando de construir un muro y le han dado una variedad de cajas de diferentes tamaños y formas. Necesita llenar un agujero particular en forma de L en la pared; ¿Debería buscar una caja en forma de L o puede sustituir dos cajas rectas del tamaño apropiado?

En el mundo funcional, la respuesta es que cualquiera de las soluciones funcionará. Al construir su mundo funcional, nunca tiene que abrir las cajas para ver qué hay dentro.

En el mundo imperativo, es peligroso construir su muro sin inspeccionar el contenido de cada caja y compararlos con el contenido de cada otra caja:

  • Algunos contienen imanes fuertes y empujarán otras cajas magnéticas fuera de la pared si se alinean incorrectamente.
  • Algunos son muy calientes o fríos y reaccionarán mal si se colocan en espacios adyacentes.

Creo que me detendré antes de perder su tiempo con metáforas más improbables, pero espero que el punto esté claro; Los ladrillos funcionales no contienen sorpresas ocultas y son completamente predecibles. Debido a que siempre puede usar bloques más pequeños del tamaño y forma correctos para sustituir a uno más grande y no hay diferencia entre dos cuadros del mismo tamaño y forma, tiene transparencia referencial. Con los ladrillos imperativos, no es suficiente tener algo del tamaño y la forma correctos, debe saber cómo se construyó el ladrillo. No es referencialmente transparente.

En un lenguaje funcional puro, todo lo que necesita ver es la firma de una función para saber qué hace. Por supuesto, es posible que desee mirar hacia adentro para ver qué tan bien funciona, pero no tiene que mirar.

En un lenguaje imperativo, nunca se sabe qué sorpresas pueden esconderse dentro.

itsbruce
fuente
"En un lenguaje funcional puro, todo lo que necesita ver es la firma de una función para saber lo que hace". - Eso no es generalmente cierto. Sí, bajo el supuesto de polimorfismo paramétrico, podemos concluir que una función de tipo (a, b) -> asolo puede ser la fstfunción y que una función de tipo a -> asolo puede ser la identityfunción, pero no se puede decir necesariamente nada sobre una función de tipo (a, a) -> a, por ejemplo.
Jörg W Mittag
2

Como entiendo más o menos el modelo de sustitución (con transparencia referencial (RT)), puede descomponer una función en sus partes más simples. Si la expresión es RT, puede descomponer la expresión y obtener siempre el mismo resultado.

Sí, la intuición es bastante correcta. Aquí hay algunos consejos para ser más precisos:

Como dijiste, cualquier expresión RT debería tener un single"resultado". Es decir, dada una factorial(5)expresión en el programa, siempre debe producir el mismo "resultado". Por lo tanto, si cierto factorial(5)está en el programa y produce 120, siempre debe producir 120 independientemente de qué "orden de pasos" se expanda / calcule, independientemente del tiempo .

Ejemplo: la factorialfunción.

def factorial(n):
    if n == 1:
        return 1
    return n * factorial(n - 1)

Hay algunas consideraciones con esta explicación.

En primer lugar, tenga en cuenta que los diferentes modelos de evaluación (ver el orden aplicativo versus el orden normal) pueden producir diferentes "resultados" para la misma expresión RT.

def first(y, z):
  return y

def second(x):
  return second(x)

first(2, second(3)) # result depends on eval. model

En el código anterior, firsty secondson referencialmente transparentes, y sin embargo, la expresión al final produce diferentes "resultados" si se evalúa bajo orden normal y orden aplicativo (bajo este último, la expresión no se detiene).

.... lo que lleva al uso de "resultado" entre comillas. Como no se requiere que una expresión se detenga, es posible que no produzca un valor. Así que usar "resultado" es algo borroso. Se puede decir que una expresión RT siempre produce lo mismo computationsbajo un modelo de evaluación.

En tercer lugar, puede ser necesario ver dos que foo(50)aparecen en el programa en diferentes ubicaciones como diferentes expresiones, cada una de las cuales produce sus propios resultados que pueden diferir entre sí. Por ejemplo, si el lenguaje permite un alcance dinámico, ambas expresiones, aunque léxicamente idénticas, son diferentes. En perl:

sub foo {
    my $x = shift;
    return $x + $y; # y is dynamic scope var
}

sub a {
    local $y = 10;
    return &foo(50); # expanded to 60
}

sub b {
    local $y = 20;
    return &foo(50); # expanded to 70
}

El alcance dinámico confunde porque hace que sea fácil pensar xque la única entrada es foo, cuando en realidad, es xy y. Una forma de ver la diferencia es transformar el programa en uno equivalente sin alcance dinámico, es decir, pasar explícitamente los parámetros, por lo que en lugar de definir foo(x), definimos foo(x, y)y pasamos yexplícitamente en las personas que llaman.

El punto es que siempre estamos bajo una functionmentalidad: dada una cierta entrada para una expresión, se nos da un "resultado" correspondiente. Si damos la misma entrada, siempre debemos esperar el mismo "resultado".

Ahora, ¿qué pasa con el siguiente código?

def foo():
   global y
   y = y + 1
   return y

y = 10
foo() # yields 11
foo() # yields 12

El fooprocedimiento rompe RT porque hay redefiniciones. Es decir, definimos yen un punto, y luego, redefinimos eso mismo y . En el ejemplo anterior de perl, los ys son enlaces diferentes aunque comparten el mismo nombre de letra "y". Aquí los ys son en realidad los mismos. Es por eso que decimos que la (re) asignación es una meta operación: de hecho, está cambiando la definición de su programa.

Aproximadamente, las personas generalmente representan la diferencia de la siguiente manera: en un entorno sin efectos secundarios, tiene un mapeo input -> output. En un entorno "imperativo", tiene input -> ouputen el contexto de una stateque puede cambiar con el tiempo.

Ahora, en lugar de simplemente sustituir las expresiones por sus valores correspondientes, uno también tiene que aplicar transformaciones a statecada operación que lo requiera (y, por supuesto, las expresiones pueden consultar eso statepara realizar cálculos).

Entonces, si en un programa libre de efectos secundarios todo lo que necesitamos saber para calcular una expresión es su entrada individual, en un programa imperativo, necesitamos conocer las entradas y el estado completo, para cada paso computacional. El razonamiento es el primero en sufrir un gran golpe (ahora, para depurar un procedimiento problemático, necesita la entrada y el volcado del núcleo). Ciertos trucos se vuelven poco prácticos, como la memorización. Pero también, la concurrencia y el paralelismo se vuelven mucho más desafiantes.

Thiago Silva
fuente
1
Es bueno que menciones la memorización. Esto se puede usar como un ejemplo de estado interno que no es visible en el exterior: una función que usa la memorización sigue siendo referencialmente transparente aunque internamente usa estado y mutación.
Giorgio