Lets say we have this function:
foo n = let comp n = n * n * n + 10
otherComp n = (comp n) + (comp n)
in (otherComp n) + (otherComp n)
How many times will comp n get actually executed? 1 or 4? Does Haskell "store" function results in the scope of let?
In GHCi, without optimization, four times.
> import Debug.Trace
> :{
| f x = let comp n = trace "A" n
| otherComp n = comp n + comp n
| in otherComp x + otherComp x
| :}
> f 10
A
A
A
A
40
With optimization, GHC might be able to inline the functions and optimize everything. However, in the general case, I would not count on GHC to optimize multiple calls into one. That would require memoizing and/or CSE (common subexpression elimination), which is not always an optimization, hence GHC is quite conservative about it.
As a thumb rule, when evaluating performance, expect that each (evaluated) call in the code corresponds to an actual call at runtime.
The above discussion applies to function bindings, only. For simple pattern bindings made of just a variable like
let x = g 20
in x + x
then g 20 will be computed once, bound to x, and then x + x will reuse the same value twice. With one proviso: that x gets assigned a monomorphic type.
If x gets assigned a polymorphic type with a typeclass constraint, then it acts as a function in disguise.
> let x = trace "A" (200 * 350)
> :t x
x :: Num a => a
> x + x
A
A
140000
Above, 200 * 350 has been recomputed twice, since it got a polymorphic type.
This mostly only happens in GHCi. In regular Haskell source files, GHC uses the Dreaded Monomorphism Restriction to provide x a monomorphic type, precisely to avoid recomputation of variables. If that can not be done, and duplicate computation is needed, GHC prefers to raise an error than silently cause recomputation. (In GHCi, the DMR is disabled to make more code work as it is, and recomputation happens, as seen above.)
Summing up: variable bindings let x = ... should be fine in source code, and work as expected without duplicating computation. If you want to be completely sure, annotate x with an explicit monomorphic type annotation.
Related
I have a few questions regarding the Just syntax in Haskell.
When question arose when I was experimenting with different ways to write a function to calculate binomial coefficients.
Consider the function:
binom :: Integer -> Integer -> Maybe Integer
binom n k | n < k = Nothing
binom n k | k == 0 = Just 1
binom n k | n == k = Just 1
binom n k | otherwise = let
Just x = (binom (n-1) (k-1))
Just y = (binom (n-1) k)
in
Just (x + y)
When I try to write the otherwise case without the let..in block without the let..in block like so:
binom n k | otherwise = (binom (n-1) (k-1)) + (binom (n-1) k)
I am faced with a compilation error No instance for (Num (Maybe Integer)) arising from a use of ‘+’. And so my first thought was that I was forgetting the Just syntax so I rewrote it as
binom n k | otherwise = Just ((binom (n-1) (k-1)) + (binom (n-1) k))
I am faced with an error even more confusing:
Couldn't match type ‘Maybe Integer’ with ‘Integer’
Expected: Maybe Integer
Actual: Maybe (Maybe Integer)
If I add Just before the binom calls, the error just compounds:
Couldn't match type ‘Maybe (Maybe Integer)’ with ‘Integer’
Expected: Maybe Integer
Actual: Maybe (Maybe (Maybe Integer))
Furthermore, if I write:
Just x = binom 3 2
y = binom 3 2
x will have the value 3 and y will have the value Just 3.
So my questions are:
Why does the syntax requite the let..in block to compile properly?
In the function, why does Just add the Maybe type when I don't use let..in?
Contrarily, why does using Just outside of the function remove the Just from the value if it's type is Just :: a -> Maybe a
Bonus question, but unrelated:
When I declare the function without the type the compiler infers the type binom :: (Ord a1, Num a2, Num a1) => a1 -> a1 -> Maybe a2. Now I mostly understand what is happening here, but I don't see why a1 has two types.
Your question demonstrates a few ways you may have got confused about what is going on.
Firstly, Just is not any kind of syntax - it's just a data constructor (and therefore also a function) provided by the standard library. The reasons your failing attempts didn't compile are therefore not due to any syntax mishaps (the compiler would report a "parse error" in this case), but - as it actually reports - type errors. In other words the compiler is able to parse the code to make sense of it, but then when checking the types, realises something is up.
So to expand on your failing attempts, #1 was this:
binom n k | otherwise = Just ((binom (n-1) (k-1)) + (binom (n-1) k))
for which the reported error was
No instance for (Num (Maybe Integer)) arising from a use of ‘+’
This is because you were trying to add the results of 2 calls to binom - which according to your type declaration, are values of type Maybe Integer. And Haskell doesn't by default know how to add two Maybe Integer values (what would Just 2 + Nothing be?), so this doesn't work. You would need to - as you eventually do with your successful attempt - unwrap the underlying Integer values (assuming they exist! I'll come back to this later), add those up, and then wrap the resulting sum in a Just.
I won't dwell on the other failing attempts, but hopefully you can see that, in various ways, the types also fail to match up here too, in the ways described by the compiler. In Haskell you really have to understand the types, and just flinging various bits of syntax and function calls about in the wild hope that the thing will finally compile is a recipe for frustration and lack of success!
So to your explicit questions:
Why does the syntax requite the let..in block to compile properly?
It doesn't. It just needs the types to match everywhere. The version you ended up with:
let
Just x = (binom (n-1) (k-1))
Just y = (binom (n-1) k)
in
Just (x + y)
is fine (from the type-checking point of view, anyway!) because you're doing as I previously described - extracting the underlying values from the Just wrapper (these are x and y), adding them up and rewrapping them.
But this approach is flawed. For one thing, it's boilerplate - a lot of code to write and try to understand if you're seeing it for the first time, when the underlying pattern is really simple: "unwrap the values, add them together, then rewrap". So there should be a simpler, more understandable, way to do this. And there is, using the methods of the Applicative typeclass - of which the Maybe type is a member.
Experienced Haskellers would write the above in one of two ways. Either:
binom n k | otherwise = liftA2 (+) (binom (n-1) (k-1)) (binom (n-1) k)
or
binom n k | otherwise = (+) <$> binom (n-1) (k-1) <*> binom (n-1) k
(the latter being in what is called the "applicative style" - if you're unfamiliar with Applicative functors there's a great introduction in Learn You a Haskell here. )
And there's another advantage of doing this compared to your way, besides the avoidance of boilerplate code. Your pattern matches in the let... in expression assume that the results of binom (n-1) (k-1) and so on are of the form Just x. But they could also be Nothing - in which case your program will crash at runtime! And exactly this will indeed happen in your case, as #chepner describes in his answer.
Using liftA2 or <*> will, due to how the Applicative instance is implemented for Maybe, avoid a crash by simply giving you Nothing as soon as one of the things you're trying to add is Nothing. (And this in turn means your function will always return Nothing - I'll leave it to you to figure out how to fix it!)
I'm not sure I really understand your questions #2 and #3, so I won't address those directly - but I hope this has given you some increased understanding of how to work with Maybe in Haskell. Finally for your last question, although it's quite unrelated: "I don't see why a1 has two types" - it doesn't. a1 denotes a single type, because it's a single type variable. You're presumably referring to the fact it has two constraints - here Ord a1 and Num a1. Ord and Num here are typeclasses - like Applicative is that I mentioned earlier (albeit Ord and Num are simpler typeclasses). If you don't know what a typeclass is I recommend reading an introductory source, like Learn You a Haskell, before continuing much further with the language - but in short it's a bit like an interface, saying that the type must implement certain functions. Concretely, Ord says the type must implement order comparisons - you need that here because you've used the < operator - while Num says you can do numeric things with it, like addition. So that type signature just makes explicit what is implicit in your function definition - the values you use this function on must be of a type that implements both order comparison and numeric operations.
binom n k | otherwise = (binom (n-1) (k-1)) + (binom (n-1) k)
You can't add two Maybe values, but you can make use of the Functor instance to add the values already wrapped in Just.
binom n k | otherwise = fmap (+) (binom (n-1) (k-1)) (binom (n-1) k)
This doesn't quite work, as eventually the recursive calls will return Nothing, and fmap (+) x y == Nothing if either x or y is Nothing. The solution is to treat two different occurrences of n < k differently.
An "initial" use can return Nothing
A "recursive" use can simply return 0, since x + 0 == x.
binom will be implemented in terms of a helper that is guaranteed to receive arguments such that n >= k.
binom :: Integer -> Integer -> Maybe Integer
binom n k | n < k = Nothing
| otherwise = Just (binom' n k)
where binom' n 0 = 1
binom' n k | n == k = 1
| otherwise = binom' (n-1) (k-1) + binom' (n-1) k
This question has received excellent answers. However, I think it is worth mentioning that you can also use a monadic do construct, like the one normally used for the “main program” of a Haskell application.
The main program generally uses a do construct within the IO monad. Here, you would use a do construct within the Maybe monad.
Your binom function can be modified like this:
binom :: Integer -> Integer -> Maybe Integer
binom n k | n < 0 = Nothing -- added for completeness
binom n k | k < 0 = Nothing -- added for completeness
binom n k | n < k = Nothing
binom n k | k == 0 = Just 1
binom n k | n == k = Just 1
binom n k | otherwise = do -- monadic do construct, within the Maybe monad
x <- (binom (n-1) (k-1))
y <- (binom (n-1) k)
return (x+y)
main :: IO ()
main = do -- classic monadic do construct, within the IO monad
putStrLn "Hello impure world !"
putStrLn $ show (binom 6 3)
If a single <- extractor fails, the whole result is Nothing.
Please recall that in that context, return is just an ordinary function, with type signature:
return :: Monad m => a -> m a
Unlike in most imperative languages, return is not a keyword, and is not part of control flow.
A key concern is that if you have many quantities that can become Nothing, the do construct looks more scalable, that is, it can become more readable than pattern matching or lift'ing functions. More details about using the Maybe monad in the online Real World Haskell book.
Note that the Haskell library provides not only liftA2, as mentioned in Robin Zigmond's answer, but also other lift'ing functions up to lift6.
Interactive testing:
You can test the thing under the ghci interpreter, like this:
$ ghci
GHCi, version 8.8.4: https://www.haskell.org/ghc/ :? for help
λ>
λ> do { n1 <- (Just 3) ; n2 <- (Just 42); return (n1+n2) ; }
Just 45
λ>
λ> do { n1 <- (Just 3) ; n2 <- (Just 42); n3 <- Nothing ; return (n1+n2+n3) ; }
Nothing
λ>
The exact semantics depend on the sort of monad involved. If you use the list monad, you get a Cartesian product of the lists you're extracting from:
λ>
λ> do { n1 <- [1,2,3] ; n2 <- [7,8,9]; return (n1,n2) ; }
[(1,7),(1,8),(1,9),(2,7),(2,8),(2,9),(3,7),(3,8),(3,9)]
λ>
I'm trying to understand laziness properly in Haskell.
I understand it such that if we have some expression where we do not actually use a sub part of the expression then that sub part will never be evaluated e.g
let x = [1..1000] in 0 will never actually evaluate the list but just return 0.
However what if i have something like the following where fib(n) is a fibonacci function and will return an error for n<0
let x = div 100 0 + (20 * 100) division by zero error
let x = fib(-3) + fib(7) n < 0 error
Will (20 * 100) and fib(7) ever get evaluated, or will it wait for the first expression to be computed and then stop after i return an error?
As per several comments, the language doesn't make many guarantees about the order of evaluation of subexpressions in a program like:
main = print $ div 100 0 + 20 * 100
So, div 100 0 could be evaluated first and throw an error before 20 * 100 is evaluated, or vice versa. Or, the whole expression could be optimized into unconditionally throwing a division by zero error without evaluating anything, which is what actually happens if you compile it with ghc -O2.
In actual fact, at least with GHC 8.6.5, the function:
foo :: Int -> Int -> Int -> Int
foo x y z = div x y + z * x
compiled with ghc -O2 produces code that attempts the division first and will throw an error if y == 0 before attempting the multiplication, so the subexpressions are evaluated in the order they appear.
HOWEVER, the function with the opposite order:
bar :: Int -> Int -> Int -> Int
bar x y z = z * x + div x y
compiled with ghc -O2 ALSO produces code that tries the division first and will throw an error if y == 0 before attempting the multiplication, so the subexpressions are evaluated in reverse order.
Moreover, even though both versions try the division before the multiplication, there's still a difference in their evaluation order -- bar fully evaluates z before trying the division, while foo evaluates the division before fully evaluating z, so if a lazy, error-generating value is passed for z, these two functions will produce different behavior. In particular,
main = print $ foo 1 0 (error "not so fast")
throws a division by zero error while:
main = print $ bar 1 0 (error "not so fast")
says "not so fast". Neither attempts the multiplication, though.
There aren't any simple rules here. The only way to see these differences is to compile with flags that dump intermediate compiler output, like:
ghc -ddump-stg -dsuppress-all -dsuppress-uniques -fforce-recomp -O2 Test.hs
and inspect the generated code.
If you want to guarantee a particular evaluation order, you need to write something like:
import Control.Parallel (pseq)
foo' :: Int -> Int -> Int -> Int
foo' x y z = let a = div x y
b = z * x
in a `pseq` b `pseq` a + b
bar' :: Int -> Int -> Int -> Int
bar' x y z = let a = z * x
b = div x y
in a `pseq` b `pseq` a + b
The function pseq is similar to the seq function discussed in the comments. The seq function would work here but doesn't always guarantee an evaluation order. The pseq function is supposed to provide a guaranteed order.
If your actual goal is to understand Haskell's lazy evaluation, rather than prevent specific subexpressions from being evaluated in case of errors in other subexpressions, then I'm not sure that looking at these examples will help much. Instead, taking a look at this answer to a related question already linked in the comments may give you better sense of how laziness "works" conceptually.
In this case expressions (20 * 100) and fib(7) will evaluation, but it is because the operator (+) firstly evaluate its second argument. If you write, for example, (20 * 100) + div 100 0, the part (20 * 100) won't evaluate. You can on your own detect which argument evaluate firstly: (error "first") + (error "second"), for example.
1.
ghci> let x = trace "one" 1 in (x, x)
(one
1,one
1)
I expected let-expr would memorize x, so the result would look like:
(one
1,1)
2.
ghci> let !x = undefined in 1
...
error
...
Ok, strictly evaluated bang-pattern.
ghci> let !x = trace "one" 1 in 1
1
Because of the strictness I expected the result would look like:
one
1
You’ve been bitten by the fact that, since GHC 7.8.1, the monomorphism restriction is disabled in GHCi by default. This means that the x binding is generalized to a polymorphic, typeclass-constrained binding with the type
x :: Num a => a
since 1 is a polymorphic number literal. Even though this type does not include any function arrows (->), at runtime, it behaves more like a function than as a value, since it is really a function that accepts a Num typeclass dictionary and uses it to construct its result.
You can avoid this by explicitly annotating the literal to avoid the polymorphism:
ghci> let x = trace "one" (1 :: Integer) in (x, x)
(one
1,1)
ghci> let !x = trace "one" (1 :: Integer) in 1
one
1
Normally, the aforementioned monomorphism restriction is in place, precisely to prevent this kind of confusion where a binding that is syntactically a value definition can have its RHS evaluated multiple times. The linked answer describes some of the tradeoffs of the restriction, but if you want, you can switch it back on, which will make your original examples do what you expect:
ghci> :set -XMonomorphismRestriction
ghci> let x = trace "one" 1 in (x, x)
(one
1,1)
ghci> let !x = trace "one" 1 in 1
one
1
I am trying to understand how where clauses evaluate in Haskell. Say we got this toy example where bar, baz and bat are defined functions somewhere:
func x = foo i j k
where
foo i j k = i + j + k
k = bat x
j = baz k
i = bar j
How does the line func x = foo i j k expand? Does it evaluate to something like func x = foo(i(j(k)), j(k), k) or func x = foo(i, j, k)?
Intro
I will assume you meant to write this code:
func :: Int -> Int
func x = foo
where
foo = i + j + k
k = bat x
j = baz k
i = bar j
This way it will type check and all three functions you defined in the where clause will eventually get called. If this is not what you meant, still do read on, as I will not only give you a depiction of the way your code evaluates, but also a method of determining the answer yourself. It may be a bit of a long story, but I hope it will worth your time.
Core
Evaluation of code absolutely depends on your choice of the compiler, but I suppose you will be using GHC, and if so, it will transform your code several times before reducing it to machine code.
First, "where clauses" will be replaced with "let clauses". This is done so as to reduce Haskell syntax to a simpler Core syntax. Core is similar enough to a math theory called lambda calculus for its eventual evaluation to proceed according to this solid foundation. At this point your code will look somewhat like this:
func = λx ->
let { k = bat x } in
let { j = baz k } in
+
(+ (bar j) j)
k
As you see, one of the function definitions from the where clause of your Haskell code disappeard altogether by the time of its arrival to Core stage (actually, it was inlined), and the others were rewritten to let notation. The binary operation (+) got rewritten to polish notation to make it unambiguous (it is now clear that i + j should be computed first). All these conversions were performed without altering the meaning of the code.
Graph machine
Then, the resulting lambda expression will be reduced to a directed graph and executed by a Spineless Tagless Graph machine. In a sense, Core to STG machine is what assembler is to Turing machine, though the former is a lambda expression, while the latter a sequence of imperative instructions. (As you may see by now, the distinction between functional and imperative languages runs rather deep.) An STG machine will translate the expressions you give it to imperative instructions that are executable on a conventional computer, through a rigorously defined operational semantics -- that is, to every syntactic feature of Core (of which it only has about 4) there is a piece of imperative assembler instructions that performs the same thing, and a Core program will be translated to a combination of these pieces.
The key feature of the operational semantics of Core is its laziness. As you know, Haskell is a lazy language. What that means is that a function to be computed and the value of this function look the same: as a sequence of bytes in RAM. As the program starts, everything is laid out as functions (closures, to be precise), but once a function's return value is computed, it will be put in the place of the closure so that all further accesses to this location in memory would immediately receive the value. In other words, a value is computed only when it is needed, and only once.
As I said, an expression in Core will be turned to a directed graph of computations that depend on each other. For example:
If you look closely, I hope this graph will remind you of the program we started with. Please note two particulars about it:
All the arrows eventually lead to x, which is consistent with our idea that supplying x is enough to evaluate func.
Sometimes two arrows lead to the same box. That means the value of this box will be evaluated once, and the second time we shall have the value for free.
So, the STG machine will take some Core code and create an executable that computes the value of a graph more or less similar to the one in the picture.
Execution
Now, as we made it to the graph, it's easy to see that the computation will proceed thus:
As func is called, the value of x is received and put in the corresponding box.
bat x is computed and put in a box.
k is set to be the same as bat x. (This step will probably be dropped by some of the optimizations GHC runs on the code, but literally a let clause requests that its value be stored separately.)
baz k is computed and put in a box.
j is set to be the same as baz k, the same as with k in step 6. bar j is computed and put in a box.
Contrary to what one would expect in the light of steps 3 and 5, i is not set to anything. As we saw in the listing of Core for our program, it was optimized away.
+ (bar j) j is computed. j is already computed, so baz k will not be called this time, thanks to laziness.
The topmost value is computed. Again, there is no need to compute bat x this time, as it was computed previously and stored in the right box.
Now, the value of func x is itself a box, ready to be used by the caller any number of times.
I would highlight that this is what's going to be happening at the time you execute the program, as opposed to compiling it.
Epilogue
That's the story, to the best of my knowledge. For further clarifications, I refer the reader to the works of Simon Peyton Jones: the book on the design of Haskell and the article on the design of Graph machine, together describing all the inner workings of GHC to the smallest peculiarity.
To review the Core generated by GHC, simply pass the flag -ddump-simpl as you compile something. It will hurt your eyes at first, but one gets used to it.
Enjoy!
postscriptum
As #DanielWagner pointed out in the comments, the laziness of Haskell has some further consequences that we should have needed to consider were we to dissect a less contrived case. Specifically: a computation may not need to evaluate some of the boxes it points to, or even any of those boxes at all. In such case, these boxes will stay untouched and unevaluated, while the computation completes and delivers its result that is in actuality independent of the subordinate boxes anyway. An example of such a function: f x = 3. This has far-reaching consequences: say, if x were impossible to compute, as in "infinite loop", a function that does not use x in the first place would not enter that loop at all. Thus, it is sometimes desirable to know in detail which sub-computations will necessarily be launched from a given computation and which may not. Such intricacies reach a bit farther than I'm prepared to describe in this answer, so at this cautionary note I will end.
The order of evaluation is not specified (in the Haskell report) for addition. As a result the evaluation order depends on the type of your number and it's Num instance.
For example, below are two types with Num instances and reversed order of evaluation. I have used a custom Show instance and debug print outs to make the point easier to see in the output.
import Debug.Trace
newtype LeftFirst = LF { unLF :: Integer }
instance Show LeftFirst where show (LF x) = x `seq` "LF"++show x
newtype RightFirst = RF { unRF :: Integer }
instance Show RightFirst where show (RF x) = x `seq` "RF"++show x
instance Num LeftFirst where
(+) a b = a `seq` LF (unLF a + unLF b)
fromInteger x = trace ("LF" ++ show x) (LF x)
instance Num RightFirst where
(+) a b = b `seq` RF (unRF a + unRF b)
fromInteger x = trace ("RF" ++ show x) (RF x)
func :: Num a => a -> a
func x = foo i j k
where
foo i j k = i + j + k
k = bat x
j = baz k
i = bar j
bar,baz,bat :: Num a => a -> a
bar = (+1)
baz = (+2)
bat = (+3)
And notice the output:
*Main> func (0 :: LeftFirst)
LF0
LF3
LF2
LF1
LF14
*Main> func (0 :: RightFirst)
RF3
RF0
RF2
RF1
RF14
First off, foo i j k will parse as ((foo i) j) k. This is because all functions in Haskell take exactly one argument. The one arg of foo is i, then the result (foo i) is a function whose one arg is j, etc. So, it's neither foo(i(j(k))) nor foo (i, j, k); however, I should warn you that ((foo i) j) k ends up being in some sense equivalent to foo (i, j, k) for reasons that we can go into if you'd like.
Second, i, j, and k will be passed to foo not as reduced values but as expressions, and it's up to foo to decide (via foo's formula) how and when to evaluate each of the supplied expressions. In the case of (+), I'm pretty sure it's simply left-to-right. So, i will be forced first, but of course to evaluate i, all the others will need to be evaluated, so you trace out the data dependency tree to its leaves, which bottoms out at x.
Perhaps the subtlety here is that there is a distinction between "reduced" and "fully reduced." i will be reduced first, in the sense that one layer of abstraction--the namei--is removed and replaced with the formula for i, but it's nor fully reduced at that point, and to fully reduce i we need to fully reduce its data dependencies.
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
I tried following expression in prelude:
let x = x in x
and I've got following exception
Exception: <<loop>>
Why is the expression recursive?
let bindings in Haskell are (mutually) recursive, meaning that you can refer to any of the defined variables/functions (things to the left of the = signs) in any of their definitions (the stuff to the right of the = sign). For the case where you have arguments (functions), this is pretty much always the intuitive expected behaviour.
let fact n = if n == 0 then 1 else n * fact (n - 1) in fact 5
In the above, you probably are not surprised that fact (n - 1) can be used in the definition of fact n. In your example, you are using x in its own definition.
When Haskell tries to evaluate let x = x in x, it keeps trying to expand x (into the RHS x) hence the loop.