Take the 2-minute tour ×

I thought that in principle haskell's type system would forbid to call impure functions (i.e. f :: a -> IO b) from pure ones, but today I realized that by calling them with return they compile just fine. In example:

h :: Maybe ()
h = do
    return $ putStrLn "???"
    return ()

Now, h works in the maybe monad, but it's a pure function nevertheless. Compiling and running it simply returns Just () as one would expect, without actually doing any I/O. I think haskell's laziness puts the things together (i.e. putStrLn's return value is not used - and can't since its value constructors are hidden and I can't pattern match against it), but why is this code legal? Are there any other reasons that makes this allowed?

As a bonus, related question: in general, is it possible to forbid at all the execution of actions of a monad from within other ones? How?

share|improve this question
add comment

2 Answers

up vote 18 down vote accepted

IO actions are first-class values like any other; that's what makes Haskell's IO so expressive, allowing you to build higher-order control structures (like mapM_) from scratch. Laziness isn't relevant here,1 it's just that you're not actually executing the action. You're just constructing the value Just (putStrLn "???"), then throwing it away.

putStrLn "???" existing doesn't cause a line to be printed to the screen. By itself, putStrLn "???" is just a description of some IO that could be done to cause a line to be printed to the screen. The only execution that happens is executing main, which you constructed from other IO actions, or whatever actions you type into GHCi. For more information, see the introduction to IO.

Indeed, it's perfectly conceivable that you might want to juggle about IO actions inside Maybe; imagine a function String -> Maybe (IO ()), which checks the string for validity, and if it's valid, returns an IO action to print some information derived from the string. This is possible precisely because of Haskell's first-class IO actions.

But a monad has no ability to execute the actions of another monad unless you give it that ability.

1 Indeed, h = putStrLn "???" `seq` return () doesn't cause any IO to be performed either, even though it forces the evaluation of putStrLn "???".

share|improve this answer
How can I give a monad the ability to execute actions from another one then, by giving it the possibility to pattern match against the values it contains? –  Riccardo Mar 22 '12 at 9:46
By writing methods that convert one monad into another, or do some execution. Control.Monad.ST.stToIO converts an ST computation into an IO computation, for example. –  Louis Wasserman Mar 22 '12 at 10:08
Crystal clear. Thank you both! –  Riccardo Mar 22 '12 at 10:33
add comment

Let's desugar!

h = do return (putStrLn "???"); return ()
-- rewrite (do foo; bar) as (foo >> do bar)
h = return (putStrLn "???") >> do return ()
-- redundant do
h = return (putStrLn "???") >> return ()
-- return for Maybe = Just
h = Just (putStrLn "???") >> Just ()
-- replace (foo >> bar) with its definition, (foo >>= (\_ -> bar))
h = Just (putStrLn "???") >>= (\_ -> Just ())

Now, what happens when you evaluate h?* Well, for Maybe,

(Just x) >>= f = f x
Nothing  >>= f = Nothing

So we pattern match the first case

f x
-- x = (putStrLn "???"), f = (\_ -> Just ())
(\_ -> Just ()) (putStrLn "???")
-- apply the argument and ignore it
Just ()

Notice how we never had to perform putStrLn "???" in order to evaluate this expression.

*n.b. It is somewhat unclear at which point "desugaring" stops and "evaluation" begins. It depends on your compiler's inlining decisions. Pure computations could be evaluated entirely at compile time.

share|improve this answer
add comment

Your Answer


