I will try to illustrate Haskell's approach (I am not sure my intuition is 100% correct since I am not a Haskell expert, corrections are welcome).
Your code can be written in Haskell as follows:
f :: Integer -> Integer -> IO Integer
f a b = do
t <- getCPUTime
return (a + b + (div t 1000000000000))
So, where is referential transparency?
f is a function that, given two integers
b, will create an action, as you can tell by the return type
This action will always be the same, given the two integers, so the function mapping a pair of integers to IO actions is referentially transparent.
When this action is executed, the integer value it produces will depend on the current CPU time: executing actions is NOT function application.
Summarizing: In Haskell you can use pure functions to construct and combine complex actions (sequencing, composing actions, and so on) in a referentially transparent way. Again, note that in the above example the pure function
f does not return an integer: it returns an action.
Some more details regarding JohnDoDo question.
What does it means that "executing actions is NOT function application"?
Given sets T1, T2, Tn, T, a function f is a mapping (relation) that associates to each tuple in T1 x T2 x ... x Tn one value in T.
So function application produces an output value given some input values.
Using this mechanism you can construct expressions that evaluate to values e.g. the value
10 is the result of evaluating the expression
4 + 6. Note that, when mapping values to values in this way, you are not performing any kind of input / output.
In Haskell, actions are values of special types which can be constructed by evaluating expressions containing appropriate pure functions that work with actions. In this way, a Haskell program is a composite action that is obtained by evaluating the
main function. This main action has type
Once this composite action has been defined, another mechanism (not function application) is used to invoke / execute the action (see e.g. here). The whole program execution is the result of invoking the main action which can in turn invoke sub-actions.
This invocation mechanism (whose internal details I do not know) takes care of performing all the needed IO calls, possibly accessing the terminal, the disk, the network, and so on.
Going back to the example.
f above does not return an integer and you cannot write a function that performs IO and returns an integer at the same time: you have to choose one of the two.
What you can do is embed the action returned by
f 2 3 into a more complex action. For example, if you want to print the integer produced by that action, you can write:
main :: IO ()
main = do
x <- f 2 3
putStrLn (show x)
do notation indicates that the action returned by the main function is obtained by a sequential composition of two smaller actions, and the
x <- notation indicates that the value produced in the first action must be passed to the second action.
In the second action
putStrLn (show x)
x is bound to the integer produced by executing the action
f 2 3
An important point is that the integer that is produced when the first action is invoked can only live inside IO actions: it can be passed from one IO action to the next but it cannot be extracted as a plain integer value.
main function above with this one:
main = do
let y = 2 + 3
putStrLn (show y)
In this case there is only one action, namely
putStrLn (show y), and
y is bound to the result of applying the pure function
+. We could also define
this main action as follows:
main = putStrLn "5"
So, notice the different syntax
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.
- In Haskell pure functions are used to build the actions that constitute a program.
- Actions are values of a special type.
- Since actions are constructed by applying pure functions, action construction is referentially transparent.
- After an action has been constructed, it can be invoked using a separate mechanism.