If I understand your question (and follow-up comments) correctly, I guess you aren't really interested in the "order of evaluation" or the details of how a particular Haskell compiler actually performs the evaluation. Instead, you're simply interested in understanding what the following program means (i.e., its "semantics"):
func x = foo i j k
where
foo i j k = i + j + k
k = bat x
j = baz k
i = bar j
so that you can predict the value of, say, func 10
. Right?
If so, then what you need to understand is:
- how names are scoped (e.g., so that you understand that the
x
in the definition of k
refers to the parameter x
in the definition of func x
and so on)
- the concept of "referential transparency", which is basically the property of Haskell programs that a variable can be replaced with its definition without affecting the meaning of the program.
With respect to variable scoping when a where
clause is involved, it's useful to understand that a where
clause is attached to a particular "binding" -- here, the where
clause is attached to the binding for func x
. The where
clause simultaneously does three things:
First, it pulls into its own scope the name of the thing that's being defined in the associated binding (here func
) and the names of any parameters (here x
). Any reference to func
or x
within the where
clause will refer to the func
and x
in the func x
binding that's being defined (assuming that the where
clause doesn't itself define new binding for func
or x
that "shadow" those binding -- that's not an issue here). In your example, the implication is that the x
in the definition k = bat x
refers to the parameter x
in the binding for func x
.
Second, it introduces into its own scope the names of all the things being defined by the where
clause (here, foo
, k
, j
, and i
), though not the parameters. That is, the i
, j
, and k
in the binding foo i j k
are NOT introduced into scope, and if you compile your program with the -Wall
flag, you'll get a warning about shadowed bindings. Because of this, your program is actually equivalent to:
func x = foo i j k
where
foo i' j' k' = i' + j' + k'
k = bat x
j = baz k
i = bar j
and we'll use this version in what follows. The implication of the above is that the k
in j = baz k
refers to the k
defined by k = bat x
, while the j
in i = bar j
refers to the j
defined by j = baz k
, but the i
, j
, and k
defined by the where
clause have nothing to do with the i'
, j'
, and k'
parameters in the binding foo i' j' k'
. Also note that the order of bindings doesn't matter. You could have written:
func x = foo i j k
where
foo i' j' k' = i' + j' + k'
i = bar j
j = baz k
k = bat x
and it would have meant exactly the same thing. Even though i = bar j
is defined before the binding for j
is given, that makes no difference -- it's still the same j
.
Third, the where
clause also introduces into the scope of the right-hand side of the associated binding the names discussed in the previous paragraph. For your example, the names foo
, k
, j
, and i
are introduced into the scope of the expression on the right hand side of the associated binding func x = foo i j k
. (Again, there's a subtlety if any shadowing is involved -- a binding in the where
clause would override the bindings of func
and x
introduced on the left-hand side and also generate a warning if compiled with -Wall
. Fortunately, your example doesn't have this problem.)
The upshot of all this scoping is that, in the program:
func x = foo i j k
where
foo i' j' k' = i' + j' + k'
k = bat x
j = baz k
i = bar j
every usage of each name refers to the same thing (e.g., all the k
names refer to the same thing).
Now, the referential transparency rule comes into play. You can determine the meaning of an expression by substituting any name by its definition (taking care to avoid name collisions or so-called "capture" of names). Therefore, if we were evaluating func 10
, it would be equivalent to:
func 10 -- binds x to 10
= foo i j k -- by defn of func
at this stage, the definition of foo
is used which binds i'
to i
, j'
to j
, and k'
to k
in order to produce the expression:
= i + j + k -- by defn of foo
= bar j + baz k + bat x -- by defs of i, j, k
= bar (baz k) + baz k + bat x -- by defn of j
= bar (baz (bat x)) + baz (bat x) + bat x -- by defn of k
= bar (baz (bat 10)) + baz (bat 10) + bat 10 -- by defn of x
So, if we defined:
bat = negate
baz y = 7 + y
bar z = 2*z
then we'd expect:
func 10 = 2 * (7 + negate 10) + (7 + negate 10) + negate 10
= -19
which is exactly what we get:
> func 10
-19