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 Maybe
s, 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.