10

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
  • 5
    It will depend on the language, some won't enforce referential transparency at all, some use the type system to separate referentially transparent functions from IO, e.g. Monads in Haskell, or uniqueness types in Clean
    – jk.
    May 9, 2013 at 12:01
  • 1
    A good type system will prevent you from calling currentDateTime from f, in languages that enforce referential transparency (such as Haskell). I'll let someone else provide a more detailed answer :) (hint: currentDateTime does IO, and this will show in its type)
    – Andres F.
    May 9, 2013 at 12:01

3 Answers 3

20

a and b are Numbers, 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
  • So it's not possible to have the compiler/type system ensure referential transparency for me in Scala like in haskell?
    – cib
    Jul 18, 2015 at 16:33
9

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
  • 2
    Would you mind detailing a bit the executing actions is NOT function application phrase? In my example I meant to return an integer. What happens if a return an integer?
    – JohnDoDo
    May 9, 2013 at 13:35
  • 2
    @JohnDoDo in Haskell with laziness at least (I can't speak to any eager referentially transparent languages) nothing is executed until it absolutely must be. This means that in the example Giorgio showed you're getting that action, and outside of doing unsavory things you can never get the number out of an IO action, rather you must combine that action with other IO actions throughout your program until you end up with Main which; surprise surprise is an IO action. Haskell itself executes the IO action, but throughout it's execution only the parts that are necessary and only when they are. May 9, 2013 at 14:08
  • @JohnDoDo If you want to return an integer, then f cannot have type IO Integer (that's an action, not an integer). But then, it cannot call the "current date", which has type IO Integer.
    – Andres F.
    May 9, 2013 at 14:30
  • Also, the IO Integer you get as output can't be converted back to a regular Integer and then be used again in pure code. Essentially, what goes on in the IO monad stays in the IO monad. (There is an exception to this, you could use unsafePerformIO to get a value back out, but by doing that you're essentially telling the compiler that "It's OK, this is really referentially transparent". The compiler will believe you, and the next time you use the function, it may fetch the value of the function it computed before, instead of using the current time.) May 9, 2013 at 16:17
  • 1
    All your last example shows is that you don't have a matching instance of Show available. But you can easily add one, in which case the code will compile and run just fine. There is nothing special about IO actions with respect to show.
    – user39685
    May 9, 2013 at 16:48
4

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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.