Learn You a Haskell for Great Good! - Miran Lipovaca [21]
Yes, we could have also defined this with a where binding. So what’s the difference between the two? At first, it seems that the only difference is that let puts the bindings first and the expression later, whereas it’s the other way around with where.
Really, the main difference between the two is that let expressions are . . . well . . . expressions, whereas where bindings aren’t. If something is an expression, then it has a value. "boo!" is an expression, as are 3 + 5 and head [1,2,3]. This means that you can use let expressions almost anywhere in your code, like this:
ghci> 4 * (let a = 9 in a + 1) + 2
42
Here are a few other useful ways to use let expressions:
They can be used to introduce functions in a local scope:
ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]
They can be separated with semicolons, which is helpful when you want to bind several variables inline and can’t align them in columns:
ghci> (let a = 100; b = 200; c = 300 in a*b*c,
let foo="Hey "; bar = "there!" in foo ++ bar)
(6000000,"Hey there!")
Pattern matching with let expressions can be very useful for quickly dismantling a tuple into components and binding those components to names, like this:
ghci> (let (a, b, c) = (1, 2, 3) in a+b+c) * 100
600
Here, we use a let expression with a pattern match to deconstruct the triple (1,2,3). We call its first component a, its second component b, and its third component c. The in a+b+c part says that the whole let expression will have the value of a+b+c. Finally, we multiply that value by 100.
You can use let expressions inside list comprehensions. We’ll take a closer look at this next.
If let expressions are so cool, why not use them all the time? Well, since let expressions are expressions, and are fairly local in their scope, they can’t be used across guards. Also, some people prefer where bindings because their variables are defined after the function they’re being used in, rather than before. This allows the function body to be closer to its name and type declaration, which can make for more readable code.
let in List Comprehensions
Let’s rewrite our previous example of calculating lists of weight/height pairs, but we’ll use a let expression inside a list comprehension instead of defining an auxiliary function with where:
calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]
Each time the list comprehension takes a tuple from the original list and binds its components to w and h, the let expression binds w / h ^ 2 to the name bmi. Then we just present bmi as the output of the list comprehension.
We include a let inside a list comprehension much as we would use a predicate, but instead of filtering the list, it only binds values to names. The names defined in this let are visible to the output (the part before the |) and everything in the list comprehension that comes after the let. So, using this technique, we could make our function return only the BMIs of fat people, like this:
calcBmis :: [(Double, Double)] -> [Double]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi > 25.0]
The (w, h) <- xs part of the list comprehension is called the generator. We can’t refer to the bmi variable in the generator, because that is defined prior to the let binding.
let in GHCi
The in part of the binding can also be omitted when defining functions and constants directly in GHCi. If we do that, then the names will be visible throughout the entire interactive session:
ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
Because we omitted the in part in our first line, GHCi knows that we’re not using zoot in that line, so it remembers it for the rest of the session. However, in the second let expression, we included the in part and called boot immediately with some parameters. A let expression that doesn’t leave out the in part is an expression in itself and represents