9
votes

I feel like this seemingly simple and essential thing is completely cryptic to me. What does 'let' expression mean? I have tried to google, but the results are full of concepts that I don't understand.

Here is some code I wrote during a lecture. It identifies if a given string is a palindrome. I don't quite understand where the return keyword is either. Is it the 'in'? What is the scope of this function? I am at a loss.

module Main where

isPalindrome :: Text -> Bool
isPalindrome text1 
  = let
    list = toString text1
    backwards = reverse list
    in list == backwards

What does 'in' mean, when it comes after 'let'?

I come from learning C#, and functional programming is unknown to me.

Thank you.

2
let creates one or more name bindings and you need another expression where these bindings are in scope and can be used.. - Iven Marquardt
Yes, in your code snippet the 'return' value (really, the evaluated expression) is the contents of the 'in' block: list == backwards. let indicates that terms in the expression (list, backwards) have bindings that you can lookup in the let. - Devon Parsons
This is equivalent to list == backwards where list = toString text1... but you might see either format (let/in vs where) depending on how the author wants the code to be read - Devon Parsons
Haskel doesn't have a "return keyword" - Fyodor Soikin
A Haskell let expression let { x1 = e1; …; xn = en } in e could be emulated in C# with a lambda expression containing a series of var declarations and a return statement, that is immediately invoked: (() => { var x1 = e1; …; var xn = en; return e; })(). The variables are in scope in the expression e, and the whole thing evaluates to e. One subtle difference is that in Haskell, any of the variables can refer to each other, while in C#, each variable can only reference those defined before it. - Jon Purdy

2 Answers

13
votes

A let-block allows you to create local variables. The general form is

let
  var1 = expression
  var2 = another expression
  var3 = stuff
in
  result expression

The result of this is whatever result expression is, but you can use var1, var2 and var3 inside result expression (and inside expression, another expression, and stuff as well). You can think of let and in as being brackets around the bunch of local variables you want to define. And yes, the thing after in is what you're returning.

10
votes

Let's look at an example using IO, as that's more like the imperative languages you're used to:

main :: IO ()
main = do
   let n = 37
   print n

Should be easy enough, right? let n = 37 introduces a local variable called n, giving it the value 37. That variable can then be used like any global definition. This is basically the same as var n = 37; or int n = 37; in C#.

But Haskell is not an imperative language, so what looks like “do this, then do that” is actually just syntactic sugar for something purely functional. In this example, the do block desugars to something looking very similar:

main = let n = 37
       in print n

The difference, and advantage, is that the variable is not introduced at some point in time as it were, which means you need to be careful about the exact order in which you do stuff, but rather it is introduced into a concrete scope. The scope of a let-bound variable includes everything that comes after the in, but also anything in the let-block itself, meaning you could also have

main = let m = n + 2
           n = 37
       in print m

(note the “reverse control flow”) or even recursive definitions

main = let l = 1 : m
           m = 2 : l
       in print $ take 15 l

(the latter will print [1,2,1,2,1,2,1,2,1,2,1,2,1])