It is indeed a result of laziness.
Laziness means that merely defining a value doesn't mean it will be evaluated; that will only happen if it's needed for something. If it's not needed, the code that would actually produce it doesn't "do anything". If a particular value is needed the code is run, but only the first time it would be needed; if there are other references to the same value and it is used again, those uses will just directly use the value that was produced the first time.
You have to remember that functions are values in every sense of the term; everything that applies to ordinary values also applies to functions. So your definition of f
is simply writing an expression for a value, the expression's evaluation will be deferred until the value of f
is actually needed, and as it's needed twice the value (function) the expression computes will be saved and reused the second time.
Lets look at it in more detail:
f=trace("f was called")$(+1)
You're defining a value f
with a simple equation (not using any syntactic sugar for writing arguments on the left hand side of the equation, or providing cases via multiple equations). So we can simply take the right hand side as a single expression that defines the value f
. Just defining it does nothing, it sits there until you call:
print $ f 1
Now print needs its argument evaluated, so this is forcing the expression f 1
. But we can't apply f
to 1
without first forcing f
. So we need to figure out what function the expression trace "f was called" $ (+1)
evaluates to. So trace
is actually called, does its unsafe IO printing and f was called
appears at the terminal, and then trace
returns its second argument: (+1)
.
So now we know what function f
is: (+1)
. f
will now be a direct reference to that function, with no need to evaluate the original code trace("f was called")$(+1)
if f
is called again. Which is why the second print
does nothing.
This case is quite different, even though it might look similar:
f n=trace("f was called:"++show n)$n+1
Here we are using the syntactic sugar for defining functions by writing arguments on the left hand side. Let's desugar that to lambda notation to see more clearly what the actual value being bound to f
is:
f = \n -> trace ("f was called:" ++ show n) $ n + 1
Here we've written a function value directly, rather than an expression that can be evaluated to result in a function. So when f
needs to be evaluated before it can be called on 1
, the value of f
is that whole function; the trace
call is inside the function instead of being the thing that is called to result in a function. So trace
isn't called as part of evaluating f
, it's called as part of evaluating the application f 1
. If you saved the result of that (say by doing let x = f 1
) and then printed it multiple times, you'd only see the one trace. But the when we come to evaluate f 2
, the trace
call is still there inside the function that is the value of f
, so when f
is called again so is trace
.
trace
has typeString -> a -> a
. The type that gets substituted fora
in thef=
case is different from the one in thef n=
case. That should also help explain the different behaviour. – Christian Sievers