What's the theoretical loophole that allows F# (or any functional language) to apply a function mulitple times on the same input

Viewed 167

In F# if I write

let p = printfn "something"

it will evaluate the expression once. Any subsequent references to p will evaluate to unit. From a theoretical perspective, the definition of a function, this makes sense. A function should only return the same result for the same input.

However if I want the side-effect to occur (i.e. the output to the screen), then I need the pass an argument to p. Typically this argument is the unit value.

let p () = printfn "something"

But why will F# evaluate the function each time, when the argument is the same each time the function is applied? Surely the same reasoning should apply as in the first case? The input to the function p doesn't change therefore there is no need to evaluate it more than once.

2 Answers

The action printfn is, strictly speaking, not a function. It is, particularly, not a pure function, because it's not referentially transparent. This is possible because F# isn't a strictly functional language (it's a 'functional first' language). It makes no explicit distinction between pure functions and impure actions.

The return value of printfn "something" is () (unit), which means that p is bound to the unit value (). The fact that something is printed on the screen is a side effect of evaluating the expression.

F# is an eagerly evaluated language. That's why you see something printed on the screen as a side effect of binding printfn "something" to p. Once the expression is evaluated, p is only bound to () - the value.

F# doesn't memoize function calls, so when you change p to a function, it'll evaluate the function every time you call it with (). Since all functions can be impure, the compiler can't tell whether or not memoization would be appropriate, so it doesn't do that.

Other languages do this in different ways. Haskell, for example, is lazily evaluated, and also explicitly distinguishes between pure functions and impure actions, so it can apply different optimization in cases like these.

To expand on the answer given in the comments, the first p is an immutable value, while the second p is a function. If you refer to an immutable value multiple times, then (obviously) its value doesn't change over time. But if you invoke a function multiple times, it executes each time, even if the arguments are the same each time.

Note that this is true even for pure functional languages, such as Haskell. If you want to avoid this execution cost, there's a specific technique called memoization that can be used to return cached results when the same inputs occur again. However, memoization has its own costs, and I'm not aware of any mainstream functional language that automatically memoizes all function calls.

Related