¿Cómo se aplica la transparencia referencial?

8

En lenguajes FP, llamar a una función con los mismos parámetros una y otra vez devuelve el mismo resultado una y otra vez (es decir, transparencia referencial).

Pero una función como esta (pseudocódigo):

function f(a, b) {
    return a + b + currentDateTime.seconds;
}

no va a devolver el mismo resultado para los mismos parámetros.

¿Cómo se manejan estos casos en FP?

¿Cómo se aplica la transparencia referencial? ¿O no es así y depende de los programadores que se comporten?

JohnDoDo
fuente
55
Dependerá del lenguaje, algunos no impondrán la transparencia referencial en absoluto, algunos usarán el sistema de tipos para separar las funciones referencialmente transparentes de IO, por ejemplo, Mónadas en Haskell, o tipos de unicidad en Clean
jk.
1
Un buen sistema de tipos le impedirá llamar currentDateTimedesde f, en idiomas que impongan transparencia referencial (como Haskell). Dejaré que alguien más proporcione una respuesta más detallada :) (pista: currentDateTimehace IO, y esto se mostrará en su tipo)
Andres F.

Respuestas:

20

ay bson Numbers, mientras que currentDateTime.secondsdevuelve un IO<Number>. Esos tipos son incompatibles, no puede agregarlos juntos, por lo tanto, su función no está bien escrita y simplemente no se compilará. Al menos así es como se hace en lenguajes puros con un sistema de tipo estático, como Haskell. En lenguajes impuros como ML, Scala o F #, corresponde al programador garantizar la transparencia referencial y, por supuesto, en lenguajes de tipo dinámico como Clojure o Scheme, no existe un sistema de tipo estático para imponer la transparencia referencial.

Jörg W Mittag
fuente
Entonces, ¿no es posible que el sistema compilador / tipo me garantice una transparencia referencial en Scala como en Haskell?
cib
8

Trataré de ilustrar el enfoque de Haskell (no estoy seguro de que mi intuición sea 100% correcta ya que no soy un experto de Haskell, las correcciones son bienvenidas).

Su código se puede escribir en Haskell de la siguiente manera:

import System.CPUTime

f :: Integer -> Integer -> IO Integer
f a b = do
          t <- getCPUTime
          return (a + b + (div t 1000000000000))

Entonces, ¿dónde está la transparencia referencial? fes una función que, dados dos enteros ay b, creará una acción, como puede ver por el tipo de retorno IO Integer. Esta acción siempre será la misma, dados los dos enteros, por lo que la función que asigna un par de enteros a acciones IO es referencialmente transparente.

Cuando se ejecuta esta acción, el valor entero que produce dependerá del tiempo actual de la CPU: la ejecución de acciones NO es una aplicación de función.

Resumiendo: en Haskell puedes usar funciones puras para construir y combinar acciones complejas (secuenciación, composición de acciones, etc.) de una manera referencialmente transparente. Nuevamente, tenga en cuenta que en el ejemplo anterior la función pura fno devuelve un entero: devuelve una acción.

EDITAR

Algunos detalles más sobre la pregunta JohnDoDo.

¿Qué significa que "ejecutar acciones NO es una aplicación de función"?

Dados los conjuntos T1, T2, Tn, T, una función f es un mapeo (relación) que se asocia a cada tupla en T1 x T2 x ... x Tn un valor en T. Entonces la aplicación de función produce un valor de salida dados algunos valores de entrada . Con este mecanismo, puede construir expresiones que evalúen valores, por ejemplo, el valor 10es el resultado de evaluar la expresión 4 + 6. Tenga en cuenta que, al asignar valores a valores de esta manera, no está realizando ningún tipo de entrada / salida.

En Haskell, las acciones son valores de tipos especiales que pueden construirse evaluando expresiones que contienen funciones puras apropiadas que funcionan con acciones. De esta manera, un programa Haskell es una acción compuesta que se obtiene al evaluar la mainfunción. Esta acción principal tiene tipo IO ().

Una vez que se ha definido esta acción compuesta, se utiliza otro mecanismo (no aplicación de función) para invocar / ejecutar la acción (ver, por ejemplo, aquí ). Toda la ejecución del programa es el resultado de invocar la acción principal que a su vez puede invocar sub-acciones. Este mecanismo de invocación (cuyos detalles internos no conozco) se encarga de realizar todas las llamadas IO necesarias, posiblemente accediendo a la terminal, el disco, la red, etc.

Volviendo al ejemplo. La función fanterior no devuelve un entero y no puede escribir una función que realice IO y devuelva un entero al mismo tiempo: debe elegir uno de los dos.

Lo que puede hacer es incrustar la acción devuelta f 2 3en una acción más compleja. Por ejemplo, si desea imprimir el número entero producido por esa acción, puede escribir:

main :: IO ()
main = do
          x <- f 2 3
          putStrLn (show x)

La donotación indica que la acción devuelta por la función principal se obtiene mediante una composición secuencial de dos acciones más pequeñas, y la x <-notación indica que el valor producido en la primera acción debe pasarse a la segunda acción.

En la segunda acción

putStrLn (show x)

el nombre xestá vinculado al entero producido al ejecutar la acción

f 2 3

Un punto importante es que el número entero que se produce cuando se invoca la primera acción solo puede vivir dentro de las acciones IO: se puede pasar de una acción IO a la siguiente pero no se puede extraer como un valor entero simple.

Compare la mainfunción anterior con esta:

main = do
      let y = 2 + 3
      putStrLn (show y)

En este caso, solo hay una acción, a saber putStrLn (show y), y yestá vinculada al resultado de aplicar la función pura +. También podríamos definir esta acción principal de la siguiente manera:

main = putStrLn "5"

Entonces, observe la sintaxis diferente

x <- f 2 3    -- Inject the value produced by an action into
              -- the following IO actions.
              -- The value may depend on when the action is
              -- actually executed. What happens when the action is
              -- executed is not known here: it may get user input,
              -- access the disk, the network, the system clock, etc.

let y = 2 + 3 -- Bind y to the result of applying the pure function `+`
              -- to the arguments 2 and 3.
              -- The value depends only on the arguments 2 and 3.

Resumen

  • En Haskell, las funciones puras se utilizan para construir las acciones que constituyen un programa.
  • Las acciones son valores de un tipo especial.
  • Como las acciones se construyen aplicando funciones puras, la construcción de acciones es referencialmente transparente.
  • Una vez que se ha construido una acción, se puede invocar usando un mecanismo separado.
Giorgio
fuente
2
¿Te importaría detallar un poco la executing actions is NOT function applicationfrase? En mi ejemplo, tenía la intención de devolver un número entero. ¿Qué sucede si un retorno es un entero?
JohnDoDo
2
@JohnDoDo en Haskell con pereza al menos (no puedo hablar con ningún lenguaje transparente referencial ansioso) no se ejecuta nada hasta que sea absolutamente necesario. Esto significa que en el ejemplo Giorgio mostraron que está recibiendo esa acción, y fuera de hacer las cosas desagradables que nunca se puede obtener el número cabo de una acción IO, en lugar de que debe combinar la acción con otras acciones IO a través de su programa hasta que se termina con principal que; sorpresa sorpresa es una acción de IO. Haskell mismo ejecuta la acción IO, pero a lo largo de su ejecución solo las partes que son necesarias y solo cuando lo son.
Jimmy Hoffa
@JohnDoDo Si desea devolver un número entero, entonces fno puede tener tipo IO Integer(eso es una acción, no un número entero). Pero entonces, no puede llamar a la "fecha actual", que tiene tipo IO Integer.
Andres F.
Además, el IO Integer que obtienes como salida no se puede volver a convertir en un entero normal y luego volver a usarse en código puro. Esencialmente, lo que sucede en la mónada IO permanece en la mónada IO. (Hay una excepción a esto, podría usar unsafePerformIO para recuperar un valor, pero al hacerlo básicamente le está diciendo al compilador que "Está bien, esto es realmente referencialmente transparente". El compilador le creerá, y el la próxima vez que use la función, puede obtener el valor de la función que calculó antes, en lugar de usar la hora actual.)
Michael Shaw
1
Todo lo que muestra su último ejemplo es que no tiene una instancia de coincidencia Showdisponible. Pero puede agregar fácilmente uno, en cuyo caso el código se compilará y se ejecutará bien. No hay nada especial en las IOacciones con respecto a show.
4

El enfoque habitual es permitir que el compilador rastree si una función es pura a través de todo el gráfico de llamadas, y rechazar el código que declara funciones como puras que hacen cosas impuras (donde "llamar a una función impura" también es una cosa impura).

Haskell hace esto haciendo todo puro en el lenguaje mismo; todo lo impuro se ejecuta en tiempo de ejecución, no el lenguaje en sí. El lenguaje simplemente construye acciones IO usando funciones puras. El tiempo de ejecución luego encuentra la función pura llamada maindesde el Mainmódulo designado , la evalúa y ejecuta la acción resultante (impura).

Otros lenguajes son más pragmáticos al respecto; Un enfoque común es agregar sintaxis para marcar funciones 'puras' y prohibir cualquier acción impura (actualizaciones variables, llamadas a funciones impuras, construcciones de E / S) dentro de dichas funciones.

En su ejemplo, currentDateTimees una función impura (o algo que se comporta como tal), por lo que está prohibido llamarlo dentro de un bloque puro y provocaría un error del compilador. En Haskell, su función se vería así:

f :: Int -> Int -> IO Int
f a b = do
    ct <- getCurrentTime
    return (a + b + timeSeconds ct)

Si trató de hacer esto en una función que no es IO, así:

f :: Int -> Int -> Int
f a b =
    let ct = getCurrentTime
    in a + b + timeSeconds ct

... entonces el compilador le dirá que sus tipos no se desprotegen, getCurrentTimees de tipo IO Time, no Time, pero timeSecondsespera Time. En otras palabras, Haskell aprovecha su sistema de tipos para modelar (y hacer cumplir) la pureza.

tdammers
fuente