22

I have a hard time grasping this. When writing in do notation, how are the following two lines different?

1. let x = expression
2. x <- expression

I can't see it. Sometimes one works, some times the other. But rarely both. "Learn you a haskell" says that <- binds the right side to the symbol on the left. But how is that different from simply defining x with let?

Saro Taşciyan
  • 5,210
  • 5
  • 31
  • 50
Undreren
  • 2,811
  • 1
  • 22
  • 34

6 Answers6

25

The <- statement will extract the value from a monad, and the let statement will not.

import Data.Typeable

readInt :: String -> IO Int
readInt s = do
  putStrLn $ "Enter value for " ++ s ++ ": "
  readLn

main = do
  x <- readInt "x"
  let y = readInt "y"
  putStrLn $ "x :: " ++ show (typeOf x)
  putStrLn $ "y :: " ++ show (typeOf y)

When run, the program will ask for the value of x, because the monadic action readInt "x" is executed by the <- statement. It will not ask for the value of y, because readInt "y" is evaluated but the resulting monadic action is not executed.

Enter value for x: 
123
x :: Int
y :: IO Int

Since x :: Int, you can do normal Int things with it.

putStrLn $ "x = " ++ show x
putStrLn $ "x * 2 = " ++ show (x * 2)

Since y :: IO Int, you can't pretend that it's a regular Int.

putStrLn $ "y = " ++ show y -- ERROR
putStrLn $ "y * 2 = " ++ show (y * 2) -- ERROR
Dietrich Epp
  • 205,541
  • 37
  • 345
  • 415
13

In a let binding, the expression can have any type, and all you're doing is giving it a name (or pattern matching on its internal structure).

In the <- version, the expression must have type m a, where m is whatever monad the do block is in. So in the IO monad, for instance, bindings of this form must have some value of type IO a on the right-hand side. The a part (inside the monadic value) is what is bound to the pattern on the left-hand side. This lets you extract the "contents" of the monad within the limited scope of the do block.

The do notation is, as you may have read, just syntactic sugar over the monadic binding operators (>>= and >>). x <- expression de-sugars to expression >>= \x -> and expression (by itself, without the <-) de-sugars to expression >>. This just gives a more convenient syntax for defining long chains of monadic computations, which otherwise tend to build up a rather impressive mass of nested lambdas.

let bindings don't de-sugar at all, really. The only difference between let in a do block and let outside of a do block is that the do version doesn't require the in keyword to follow it; the names it binds are implicitly in scope for the rest of the do block.

bitbucket
  • 1,307
  • 7
  • 11
9

In the let form, the expression is a non-monadic value, while the right side of a <- is a monadic expression. For example, you can only have an I/O operation (of type IO t) in the second kind of binding. In detail, the two forms can be roughly translated as (where ==> shows the translation):

do {let x = expression; rest} ==> let x = expression in do {rest}

and

do {x <- operation; rest} ==> operation >>= (\ x -> do {rest})
Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
Jeremiah Willcock
  • 30,161
  • 7
  • 76
  • 78
3

let just assigns a name to, or pattern matches on arbitrary values.

For <-, let us first step away from the (not really) mysterious IO monad, but consider monads that have a notion of a "container", like a list or Maybe. Then <- does not more than "unpacking" the elements of that container. The opposite operation of "putting it back" is return. Consider this code:

add m1 m2 = do
   v1 <- m1
   v2 <- m2
   return (v1 + v2) 

It "unpacks" the elements of two containers, add the values together, and wraps it again in the same monad. It works with lists, taking all possible combinations of elements:

main  = print $ add [1, 2, 3] [40, 50]
--[41,51,42,52,43,53]

In fact in case of lists you could write as well add m1 m2 = [v1 + v2 | v1 <- m1, v2 <- m2]. But our version works with Maybes, too:

main  = print $ add (Just 3) (Just 12)
--Just 15
main  = print $ add (Just 3) Nothing
--Nothing

Now IO isn't that different at all. It's a container for a single value, but it's a "dangerous" impure value like a virus, that we must not touch directly. The do-Block is here our glass containment, and the <- are the built-in "gloves" to manipulate the things inside. With the return we deliver the full, intact container (and not just the dangerous content), when we are ready. By the way, the add function works with IO values (that we got from a file or the command line or a random generator...) as well.

Landei
  • 54,104
  • 13
  • 100
  • 195
2

Haskell reconciles side-effectful imperative programming with pure functional programming by representing imperative actions with types of form IO a: the type of an imperative action that produces a result of type a.

One of the consequences of this is that binding a variable to the value of an expression and binding it to the result of executing an action are two different things:

x <- action        -- execute action and bind x to the result; may cause effect
let x = expression -- bind x to the value of the expression; no side effects

So getLine :: IO String is an action, which means it must be used like this:

do line <- getLine -- side effect: read from stdin
   -- ...do stuff with line

Whereas line1 ++ line2 :: String is a pure expression, and must be used with let:

do line1 <- getLine            -- executes an action
   line2 <- getLine            -- executes an action
   let joined = line1 ++ line2 -- pure calculation; no action is executed
   return joined
Luis Casillas
  • 29,802
  • 7
  • 49
  • 102
2

Here is a simple example showing you the difference. Consider the two following simple expressions:

letExpression = 2
bindExpression = Just 2

The information you are trying to retrieve is the number 2. Here is how you do it:

let x = letExpression
x <- bindExpression

let directly puts the value 2 in x. <- extracts the value 2 from the Just and puts it in x.

You can see with that example, why these two notations are not interchangeable:

let x = bindExpression would directly put the value Just 2 in x. x <- letExpression would not have anything to extract and put in x.

Nicolas Dudebout
  • 9,172
  • 2
  • 34
  • 43