In FP languages, calling a function with the same parameters over and over again returns the same result over and over again (i.e. referential transparency).
But a function like this (pseudo-code):
function f(a, b) {
return a + b + currentDateTime.seconds;
}
is not going to return the same result for the same parameters.
How are these cases handled in FP?
How is referential transparency enforced? Or is it not and it depends on the programmers to behave themselves?
2
a
and b
are Number
s, whereas currentDateTime.seconds
returns an IO<Number>
. Those types are incompatible, you cannot add them together, therefore your function is not well-typed and simply won’t compile. At least that’s how it’s done in pure languages with a static type system, like Haskell. In impure languages like ML, Scala or F#, it is up to the programmer to ensure referential transparency, and of course in dynamically typed languages like Clojure or Scheme, there is no static type system to enforce referential transparency.
1
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:
import System.CPUTime
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 a
and b
, will create an action, as you can tell by the return type IO Integer
.
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.
EDIT
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 IO ()
.
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.
The function 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)
The 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)
the name 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.
Compare the 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.
Summary
- 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.
7
The usual approach is to allow the compiler to track whether or not a function is pure through the entire call graph, and reject code that declares functions as pure that do impure things (where “calling an impure function” is also an impure thing).
Haskell does this by making everything pure in the language itself; anything impure runs in the runtime, not the language itself. The language merely constructs IO actions using pure functions. The runtime then finds the pure function called main
from the designated Main
module, evaluates it, and executes the resulting (impure) action.
Other languages are more pragmatic about it; a common approach is to add syntax for marking functions ‘pure’, and forbidding any impure actions (variable updates, calling impure functions, I/O constructs) inside such functions.
In your example, currentDateTime
is an impure function (or something that behaves like one), so calling it inside a pure block is forbidden and would cause a compiler error. In Haskell, your function would look something like this:
f :: Int -> Int -> IO Int
f a b = do
ct <- getCurrentTime
return (a + b + timeSeconds ct)
If you tried to do this in a non-IO function, like this:
f :: Int -> Int -> Int
f a b =
let ct = getCurrentTime
in a + b + timeSeconds ct
…then the compiler would tell you that your types don’t check out – getCurrentTime
is of type IO Time
, not Time
, but timeSeconds
expects Time
. In other words, Haskell leverages its type system to model (and enforce) purity.