Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?
like image 691
TheMP Avatar asked Jun 18 '14 19:06

TheMP


1 Answers

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.

like image 177
leftaroundabout Avatar answered Sep 27 '22 18:09

leftaroundabout