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?
f
es una función que, dados dos enteros a
y 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 f
no 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 10
es 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 main
funció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 f
anterior 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 3
en 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 do
notació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 x
está 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 main
funció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 y
está 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.