77

Working code:

import System
main = do
     [file1, file2] <- getArgs
     --copy file contents
     str <- readFile file1
     writeFile file2 str

Crashing code:

import System
main = do
       [file1, file2] = getArgs
       str = readFile file1
       writeFile file2 str

When I tried, it threw an error as:

a.hs:6:18: parse error on input '='

So, how different is <- from =?

Rahn
  • 4,787
  • 4
  • 31
  • 57
  • 12
    You need to understand Monad de-sugaring to get an actual idea of how this fits. – Sibi Feb 20 '15 at 08:36
  • 8
    @Sibi that's technically true but I feel it should be possible to answer this question without mentioning the M-word. After all, m***ds are just one way of dealing with IO in a pure language. Haskell would still have a separation between pure and impure code even if it didn't have m****ds built into the language, and this problem (or one like it) would still exist. – Chris Taylor Feb 20 '15 at 09:49
  • 1
    @ChrisTaylor While it's possible to answer this question without explaining Monad, I really doubt if it will help the OP in grasping the concept. That being said, there are lot of good answers down here. – Sibi Feb 20 '15 at 09:59

4 Answers4

133

To understand the real difference, you have to understand monads, and the desugaring described by @rightfold in their answer.

For the specific case of the IO monad, as in your getArgs example, a rough but useful intuition can be made as follows:

  • x <- action runs the IO action, gets its result, and binds it to x
  • let x = action defines x to be equivalent to action, but does not run anything. Later on, you can use y <- x meaning y <- action.

Programmers coming from imperative languages which allow closures, may draw this rough parallel comparison with JavaScript:

var action = function() { print(3); return 5; }

// roughly equivalent to x <- action
print('test 1')
var x = action()  // output:3
// x is 5

// roughly equivalent to let y = action
print('test 2')
var y = action    // output: nothing
// y is a function

// roughly equivalent to z <- y
print('test 3')
var z = y()       // output:3
// z is 5

Again: this comparison focuses on IO, only. For other monads, you need to check what >>= actually is, and think about the desugaring of do.

Mark Amery
  • 143,130
  • 81
  • 406
  • 459
chi
  • 111,837
  • 3
  • 133
  • 218
65
do
    x <- y
    f x

is equivalent to:

y >>= \x -> f x

do
    let x = y
    f x

is equivalent to

f y

i.e. let/= does no monadic bind whereas <- does.

  • Saying `let x = y in f x` (which really *is* equivalent to the 2nd do expression) is "equivalent" to `f y` is dangerous, mainly because it may not be obvious to the reader what "equivalent" means; the values of these two expressions are the same, but their semantics may be different. – user2407038 Feb 20 '15 at 23:43
  • 11
    @user2407038 no, they are exactly the same thing. There isn't a single difference. –  Feb 20 '15 at 23:47
  • this give me a hard time (simplified version) `testF box = do a <- box; let b = do x <- Just "from let"; return x; return (fmap (++ box) b)` the let smuggle a monad without any additional return, b is put `silently` in the box. In the same time I tried with `... b <- do x <- Just "from let"; return x ..."` here it compiles but I didn't find what to give to testF to run it:-) – user3680029 Nov 27 '19 at 16:03
23

The code doesn't compile because the types don't match. Let's load up a GHCI session and look at the types of the functions you're using -

> :t writeFile
writeFile :: FilePath -> String -> IO ()
>
> :t readFile
readFile :: FilePath -> IO String

So writeFile wants a FilePath and a String. You want to get the String from readFile - but readFile returns IO String instead of String.

Haskell is a very principled language. It has a distinction between pure functions (which give the same outputs every time they are called with the same arguments) and impure code (which may give different results, e.g. if the function depends on some user input). Functions that deal with input/output (IO) always have a return type which is marked with IO. The type system ensures that you cannot use impure IO code inside pure functions - for example, instead of returning a String the function readFile returns an IO String.

This is where the <- notation is important. It allows you to get at the String inside the IO and it ensures that whatever you do with that string, the function you are defining will always be marked with IO. Compare the following -

> let x = readFile "tmp.txt"
> :t x
x :: IO String

which isn't what we want, to this

> y <- readFile "tmp.txt"
> :t y
y :: String

which is what we want. If you ever have a function that returns an IO a and you want to access the a , you need to use <- to assign the result to a name. If your function doesn't return IO a , or if you don't want to get at the a inside the IO then you can just use =.

Chris Taylor
  • 46,912
  • 15
  • 110
  • 154
  • 2
    The OP's code has a *syntax error*. It seems that should probably be addressed before the type error that hides behind it. – dfeuer Sep 14 '16 at 04:25
  • 1
    it could be stressed that having to go through the `<-` to get to the computed result *forces* one to do this *inside* the IO `do` block, to be able to do this. thus enforcing the separation of impure IO from pure code. – Will Ness May 09 '18 at 15:21
19
let x = readFile file1

This takes the action "readFile file1" and stores the action in x.

x <- readFile file1

This executes the action "readFile file1" and stores the result of the action in x.

In the first example, x is an unexecuted I/O action object. In the second example, x is the contents of a file on disk.

MathematicalOrchid
  • 61,854
  • 19
  • 123
  • 220