Using return vs. not using return in the list monad
Asked Answered
N

6

39

I started my Grand Haskell Crusade (GHC :) ) and I am a bit confused with monads and IO functions. Could anyone explain simply what is the difference between those two functions?

f1 = do x <- [1,2]
        [x, x+1] -- this is monad, right?

f2 = do x <- [1,2]
        return [x, x+1]

The results are:

*Main> f1
[1,2,2,3]

*Main> f2
[[1,2],[2,3]]
Nagano answered 4/7, 2012 at 6:7 Comment(6)
In addition to these excellent answers, I'd like to point out that you must not confuse return in the Monad class with the return keyword in C-like imperative languages. They are completely different, but quite a lot of people used to those languages see return and don't realise that it's not causing a value to be returned, it's making a computation which will return that value when evaluated at some later point. Being a functional language, Haskell has no need for anything resembling a return statement.Ronnieronny
Indeed, Haskell's choice to use the word return was, imho, a huge mistake. But due to backwards compatibility, there's no going back on it now!Octan
@DanBurton: you can always make an alias :) do_not_return a = return aNagano
There is already an alias that works with most Monad types: pure.Bricker
@JesseHallett "pure" is also a bad name. pure x implies taking an x and making it "pure" (like sin x takes x and returns the sine of x). but in fact it's the opposite: it takes a "pure" x.Loden
f1 = do { x <- [1,2] ; [x, x+1] } = do { x <- [1,2] ; r <- [x, x+1] ; return r} = join (do { x <- [1,2] ; return [x, x+1] }) = concat f2.Loden
B
30

The other answers here are correct, but I wonder if they're not quite what you need... I'll try to keep this as simple as possible, just two points:


Point 1. return is not a special thing in the Haskell language. It's not a keyword, and it's not syntactic sugar for something else. It's just a function that's part of the Monad typeclass. Its signature is simply:

return :: a -> m a

where m is whichever monad we're talking about at the time. It takes a "pure" value and jams it into your monad. (Incidentally, there's another function called pure that's basically a synonym for return... I like it better because the name is more obvious!) Anyway, if m is the list monad, then return has this type:

return :: a -> [a]

If it helps, you could think of the type synonym type List a = [a], which might make it slightly more obvious that List is the thing we're substituting for m. Anyway, if you were going to implement return yourself, the only reasonable way you'd implement it is by taking some value (of whatever type a) and sticking it in a list by itself:

return a = [a]

So I can say return 1 in the list monad, and I'll get [1]. I can likewise say return [1, 2, 3] and I'll get [[1, 2, 3]].


Point 2. IO is a monad, but not all monads are IO. Many Haskell tutorials seem to conflate the two topics largely for historical reasons (incidentally, the same confusing historical reasons that led to return being so poorly named). It sounds like you might have some (understandable) confusion around that.

In your code, you're in the list monad because you wrote do x <- [1, 2]. If instead you had written do x <- getLine for example, you'd be in the IO monad (because getLine returns IO String). Anyway, you're in the list monad, so you get list's definition of return described above. You also get list's definition of >>=, which is just (a flipped version of) concatMap, defined as:

concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)

The other posted answers pretty much have it covered from here :) I know I didn't answer your question directly, but I'm hoping these two points instead addressed the fundamental things you might have found confusing.

Biondo answered 5/7, 2012 at 4:25 Comment(0)
M
44

To see why you get the particular answers that arise, the desugaring explanations are very helpful. Let me supplement them with a little general advice about developing perceptions of Haskell code.

Haskell's type system makes no distinction between two separable "moral" purposes:

  • [x] the type of values which are lists with elements drawn from x
  • [x] the type of computations of elements of x which allow prioritized choice

The fact that these two notions have the same representation does not mean that they play the same roles. In f1, the [x, x+1] is playing the role of computation, so the possibilities it generates are merged into the choice generated by the whole computation: that's what the >>= of the list monad does. In f2, however, the [x, x+1] is playing the role of value, so that the whole computation generates a prioritized choice between two values (which happen to be list values).

Haskell does not use types to make this distinction [and you may have guessed by now that I think it should, but that's another story]. Instead, it uses syntax. So you need to train your head to perceive the value and computation roles when you read code. The do notation is a special syntax for constructing computations. What goes inside the do is built from the following template kit:

jigsaw pieces for computations

The three blue pieces make do-computations. I've marked the computation holes in blue and the value holes in red. This is not meant to be a complete syntax, just a guide to how to perceive pieces of code in your mind.

Indeed, you may write any old expression in the blue places provided it has a suitably monadic type, and the computation so generated will be merged into the overall computation using >>= as needed. In your f1 example, your list is in a blue place and treated as prioritized choice.

Similarly, you may write expressions in red places which may very well have monadic types (like lists in this case), but they will be treated as values all the same. That's what happens in f2: as it were, the result's outer brackets are blue, but the inner brackets are red.

Train your brain to make the value/computation separation when you read code, so that you know instinctively which parts of the text are doing which job. Once you've reprogrammed your head, the distinction between f1 and f2 will seem completely normal!

Mehala answered 4/7, 2012 at 9:46 Comment(10)
Re "Haskell does not use types to make this distinction [and you may have guessed by now that I think it should...": Surely part of the point of the monad concept is that these types we have lying around anyway like list, maybe, etc, happen to to also be monads because they support the right operations, and therefore can be used as both monads and container types. Are you advocating that we don't make list a monad, and instead invent a new type identical to list and make that a monad, so that we can't mix normal list operations and monad operations?Mackey
@Mackey yes, that seems to be what he's advocating. We do make newtype Ziplist a ..., why the asymmetry? There are several bind implementations possible besides plain concatMap, say one which alternates between the streams, (`bind_i`f) = foldr (++/) [] . fmap f where (x:xs) ++/ ys = x:ys ++/ xs. We'd have to newtype that too, so why the asymmetry? That seems to be the argument. I suspect on a deeper level it is a distinction between list as concrete type and as an abstract type. We could implement lists over balanced binary trees, and would have to replicate monad definition code.Loden
@WillNess It's true that at most one "version" of the list monad can be used directly on a native list, so either there's an asymmetry between that "default" instance and the extra newtype ones. We could avoid that by always making a new type, but this has nothing to do with monads, so we should probably do it for all type classes, which sounds painful to me.Mackey
@WillNess Probably a more serious point is that even if we did so, I don't use the list monad as just a monad and nothing more, I use it as a type that supports both monadic and list operations, so I'd want to have a way of lifting the list operations to work on the special monad type, and then we're right back at square one with monad and list operations being used on the same type.Mackey
@WillNess In what way does that seem to be what I'm advocating? I was quite careful not to say what I'm advocating. Rest assured that I am not advocating what you suggest I might be. The issue is how to cut up a type as "effect permissions" and "value type". Haskell allows two ways to do this: in red places, the only effect is partiality and the value part is the whole type; in blue places, the effect part is applied to the value part. The syntax of terms selects which analysis of types is used. I'd rather extend the syntax of types to mark the effect/value boundary, but in a renegotiable way.Mehala
@Mehala I could've easily be mistaken. It seems that I was. My bad.Loden
@WillNess No worries. My experimental language, Frank, is closer to what I have in mind. It allows you to change locally the "ambient monad" in which normal applicative syntax is interpreted. There's no need for return or explicit lifting from tame monads to wild ones, but there is explicit thunking (suspending computation as value) and forcing (running suspended computation). It's fun, but it might be hard to retrofit.Mehala
I'm just an unsophisticated user who very much dislikes the monad syntax and can't stand imperative style (do notation). I would rather have Haskell deduce the monadic chains by itself, and have the monad implementations plugged in later, even at run-time (kind of like linker). That way we'd have a "random" computation, say, and could plug in different implementations of random at run-time; or have "windowing" and re-plug in different window managers as we please; etc. Make it more modular. (it's a vague idea). Mostly, I'd expect it to combine different monads by itself (say rand+window).Loden
(contd.) I'd just want to write simple stuff like e.g. fun x=x+rand 32 and have its monadic type deduced for me automatically, just by virtue of rand belonging to a Random monad, say. Then I could use this fun pseudo-function anywhere, and automatically have "monadic contagion" so to speak, without having to use tortured syntax. And if I used actions from several different monads, I'd like them to get mixed and combined for me automatically, like, say, if I later wrote g z = makeWindow (fun z) etc. g would carry around two "sockets" to be plugged, so to speak.Loden
@WillNess I'm with you on this. Frank has more of that flavour (although at the moment it does effect checking rather than inference).Mehala
B
30

The other answers here are correct, but I wonder if they're not quite what you need... I'll try to keep this as simple as possible, just two points:


Point 1. return is not a special thing in the Haskell language. It's not a keyword, and it's not syntactic sugar for something else. It's just a function that's part of the Monad typeclass. Its signature is simply:

return :: a -> m a

where m is whichever monad we're talking about at the time. It takes a "pure" value and jams it into your monad. (Incidentally, there's another function called pure that's basically a synonym for return... I like it better because the name is more obvious!) Anyway, if m is the list monad, then return has this type:

return :: a -> [a]

If it helps, you could think of the type synonym type List a = [a], which might make it slightly more obvious that List is the thing we're substituting for m. Anyway, if you were going to implement return yourself, the only reasonable way you'd implement it is by taking some value (of whatever type a) and sticking it in a list by itself:

return a = [a]

So I can say return 1 in the list monad, and I'll get [1]. I can likewise say return [1, 2, 3] and I'll get [[1, 2, 3]].


Point 2. IO is a monad, but not all monads are IO. Many Haskell tutorials seem to conflate the two topics largely for historical reasons (incidentally, the same confusing historical reasons that led to return being so poorly named). It sounds like you might have some (understandable) confusion around that.

In your code, you're in the list monad because you wrote do x <- [1, 2]. If instead you had written do x <- getLine for example, you'd be in the IO monad (because getLine returns IO String). Anyway, you're in the list monad, so you get list's definition of return described above. You also get list's definition of >>=, which is just (a flipped version of) concatMap, defined as:

concatMap :: (a -> [b]) -> [a] -> [b]
concatMap f xs = concat (map f xs)

The other posted answers pretty much have it covered from here :) I know I didn't answer your question directly, but I'm hoping these two points instead addressed the fundamental things you might have found confusing.

Biondo answered 5/7, 2012 at 4:25 Comment(0)
L
24

It's easy to see when re-writing the code with bind and return:

[1,2] >>= (\x->        [x,x+1]) === concatMap (\x-> [  x,x+1  ]) [1,2]

[1,2] >>= (\x-> return [x,x+1]) === concatMap (\x-> [ [x,x+1] ]) [1,2]

Your first code is tantamount to calling join on the results of the second, removing one monadic "layer" introduced by return :: a -> m a, conflating the "list" of the monad in use, with the "list" of your value. If you were returning a pair, say, it wouldn't have made much sense to omit the return:

                                     -- WRONG: type mismatch
[1,2] >>= (\x->        (x,x+1)) === concatMap (\x-> (  x,x+1  )) [1,2]
                                     -- OK:
[1,2] >>= (\x-> return (x,x+1)) === concatMap (\x-> [ (x,x+1) ]) [1,2]

Or, we can use a join/fmap re-write:

ma >>= famb === join (fmap famb ma)   -- famb :: a -> m b, m ~ []

join (fmap (\x->        [x,x+1]) [1,2]) = concat [ [  x,x+1  ] | x<-[1,2]]
join (fmap (\x->        (x,x+1)) [1,2]) = concat [ (  x,x+1  ) | x<-[1,2]]  -- WRONG
join (fmap (\x-> return [x,x+1]) [1,2]) = concat [ [ [x,x+1] ] | x<-[1,2]]

                                                         =  [y | x<-[1,2], y<-[ x,x+1 ]]
                                            {- WRONG -}  =  [y | x<-[1,2], y<-( x,x+1 )]
                                                         =  [y | x<-[1,2], y<-[[x,x+1]]]
Loden answered 4/7, 2012 at 6:28 Comment(1)
the first code, by Monad laws, is equivalent to do {x <- [1,2]; r <- [x, x+1]; return r} which, with Monad Comprehensions, is written [ r | x <- [1,2], r <- [x,x+1] ]; the second is [ [x,x+1] | x <- [1,2] ]. so, f1 = flip ($) <$> [1,2] <*> [id,(1+)] and f2 = (\x->[x,x+1]) <$> [1,2]. -- with the tuple as in the answer we get flip ($) <$> [1,2] <*> (id,(1+)) (wrong) and (\x->(x,x+1)) <$> [1,2] (OK).Loden
J
14

The List type ([]) is a monad, yes.

Now, remember what return does. This is easy to see from its type signature: return :: Monad m => a -> m a. Let's substitute the list type in: return :: a -> [a]. So this function takes some value and returns just a list of that value. It's equivalent to \ x -> [x].

So in the first code sample, you have one list at the very end: [x, x+1]. In the second sample, you have a nested list: one list comes from [x, x + 1] and another list comes from return. The line return [x, x + 1] could be rewritten to [[x, x + 1]] in this case.

At the end, the result is the list of all possible results. That is, we concatenate the result of x as 1 and the result of x as 2 (thanks to the x <- [1,2] line). So in the first case, we concatenate two lists; in the second case, we concatenate two lists of lists, because the extra return wrapped the result in an extra list.

Jodee answered 4/7, 2012 at 6:30 Comment(0)
C
10

Desugaring the do syntax to the equivalent

f1 = [1,2] >>= \x -> [x, x+1]
f2 = [1,2] >>= \x -> return [x, x+1]

Now, >>= comes from the Monad class,

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

and the LHS of >>= in both f1 and f2 is [a] (where a has defaulted to Integer), so we're really considering

instance Monad [] where
    (>>=) :: [a] -> (a -> [b]) -> [b]
    ...

This obeys the same monad laws as, but is a different monad from,

instance Monad IO where ...

and >>= for other monads, so don't just blindly apply what you know about one to the other, okay? :)

instance Monad [] is defined thusly in GHC

instance Monad [] where
    m >>= k = foldr ((++) . k) [] m
    return x = [x]
    ...

but it is probably easier to understand []'s >>= as

instance Monad [] where
    m >>= k = concatMap k m

If you take this and apply it to the original, you get

f1 = concatMap (\x -> [x, x+1]) [1,2]
f2 = concatMap (\x -> [[x, x+1]]) [1,2]

and it's clear why the values of f1 and f2 are what they are.

Callas answered 4/7, 2012 at 6:30 Comment(0)
E
1

My understanding is that doing

return [1,2] when in the List monad is the same as doing

func :: Maybe (Maybe Int)
func = return $ Just 1

which is why you end up with wrapped lists, as [] are just syntactic sugar right?

When really he wanted to do

func :: Maybe Int
func = return 5

or

func = Just 5

Its a bit easier to see whats going on with the Maybe monad I think.

So when you do

return [1,2]

You are doing the same as

[ [1,2] ]
Electrodynamics answered 5/7, 2012 at 15:31 Comment(1)
return Just 1 parses like (return Just) 1 which doesn't work :(Callas

© 2022 - 2024 — McMap. All rights reserved.