As I am working on learning Haskell, I understand it is a purely functional language. I am having trouble understanding why let
-statements don\'t violate purity
let
introduces a new local variable with a single unalterable value, and it has more local scope than any surrounding definitions, so for example:
*Main> (let length = 2 in show length) ++ ' ':show (length "Hello")
"2 5"
Here the first length
has the value 2, but its scope local to the brackets. Outside the brackets, length
means what it has always meant. Nothing has been edited, just a more local variable has been introduced that happens to have the same name as another one in a different scope. Let's make ghci mad by omitting the brackets and making it try to make length
a number and a function:
*Main> let length = 2 in show length ++ ' ':show (length "Hello")
<interactive>:1:14:
No instance for (Num ([Char] -> a0))
arising from the literal `2'
Possible fix: add an instance declaration for (Num ([Char] -> a0))
In the expression: 2
In an equation for `length': length = 2
In the expression:
let length = 2 in show length ++ ' ' : show (length "Hello")
<interactive>:1:19:
No instance for (Show ([Char] -> a0))
arising from a use of `show'
Possible fix: add an instance declaration for (Show ([Char] -> a0))
In the first argument of `(++)', namely `show length'
In the expression: show length ++ ' ' : show (length "Hello")
In the expression:
let length = 2 in show length ++ ' ' : show (length "Hello")
And here's your example:
*Main> let e = exp 1 in show e ++ " " ++ let e = 2 in show e
"2.718281828459045 2"
I'll add brackets to emphasise the scope:
*Main> let e = exp 1 in (show e ++ " " ++ (let e = 2 in (show e)))
"2.718281828459045 2"
The first e
is hidden rather than edited. Referential transparency is preserved, but it's definitely bad practice because it's hard to follow.
Now secretly the interactive prompt is a bit like one big do
block in the IO monad, so let's look at that:
testdo = do
let e = exp 1
print e
let e = 2
print e
Now I have to admit that looks an awful lot like breaking referential transparency, but bear in mind that this looks like it does too:
testWrite = do
writeFile "test.txt" "Hello Mum"
xs <- readFile "test.txt"
print xs
writeFile "test.txt" "Yo all"
xs <- readFile "test.txt"
print xs
Now in what sense have we got referential transparency? xs
clearly refers to two different strings. Well, what does this do
notation actually mean? It's syntactic sugar for
testWrite = writeFile "test.txt" "Hello Mum"
>> readFile "test.txt"
>>= (\xs -> print xs
>> writeFile "test.txt" "Yo all"
>> readFile "test.txt"
>>= (\xs -> print xs))
Now it's clearer that what looks like assignment is just local scope again. You presumably are happy to do
increment :: [Int] -> [Int]
increment = \x -> map (\x -> x+1) x
Which is doing the same thing.
Summary
What appeared to be assignment is just introduction of a new local scope. Phew. If you use this a lot, you make it very unclear what your code means.
Your second let
creates a new binding for e
that shadows the existing variable. It does not modify e
. You can easily check this with the following:
Prelude> let e = 1
Prelude> let f () = "e is now " ++ show e
Prelude> f ()
"e is now 1"
Prelude> let e = 2
Prelude> e
2
Prelude> f ()
"e is now 1"
Prelude>