haskellfunctional-programmingdo-notation

Understanding I/O monad and the use of "do" notation


I am still struggling with Haskell and now I have encountered a problem with wrapping my mind around the Input/Output monad from this example:

main = do   
line <- getLine  
if null line  
    then return ()  
    else do  
        putStrLn $ reverseWords line  
        main  
  
reverseWords :: String -> String  
reverseWords = unwords . map reverse . words

I understand that because functional language like Haskell cannot be based on side effects of functions, some solution had to be invented. In this case it seems that everything has to be wrapped in a do block. I get simple examples, but in this case I really need someone's explanation:

  1. Why isn't it enough to use one, single do block for I/O actions?
  2. Why do you have to open completely new one in if/else case?
  3. Also, when does the -- I don't know how to call it -- "scope" of the do monad ends, i.e. when can you just use standard Haskell terms/functions?

Solution

  • The do block concerns anything on the same indentation level as the first statement. So in your example it's really just linking two things together:

     line <- getLine
    

    and all the rest, which happens to be rather bigger:

     if null line  
      then return ()
      else do
          putStrLn $ reverseWords line  
          main  
    

    but no matter how complicated, the do syntax doesn't look into these expressions. So all this is exactly the same as

    main :: IO ()
    main = do
       line <- getLine
       recurseMain line
    

    with the helper function

    recurseMain :: String -> IO ()
    recurseMain line
       | null line  = return ()
       | otherwise  = do
               putStrLn $ reverseWords line
               main
    

    Now, obviously the stuff in recurseMain can't know that the function is called within a do block from main, so you need to use another do.