Strange monadic behaviour

Viewed 75

I tried to write a program which takes filepaths as (command line) arguments and returns the first line of each file

main = getArgs >>= ( mapM_ ( \file -> ( openFile file ReadMode >>= ( (\handle -> hGetLine handle >>= print)  >> hClose )  ) ) ) 

I know that this doesn't look very beautiful but I am just a beginner in Haskell. I did also avoid the do notation on purpose because I just don't feel very comfortable with her (yet).

So the Code above compiles and returns an error for invalid file paths, and nothing (i.e. especially not the first line of a file) for valid paths.

I must confess that I have pretty much no idea what I did wrong, but I made the following observation:
If I add the following to check which parts still get executed

main = getArgs >>= ( mapM_ ( \file -> ( openFile file ReadMode >>= ( (\handle -> hGetLine handle >>= print) >> (const $ putStr "Hello1")  >> hClose >> (const $ putStr "Hello2") )  ) ) ) 

the program prints only the second "Hello", this reminded me of the type signature of (>>):

(>>) :: Monad m => m a -> m b -> m b

so taking into perspective that only something of the type of the second argument gets returned, maybe the first argument is just ignored?
But the first argument against this theory is that such a function would not seem to be very useful (at least not in the context of the IO Monad), and the second is that the program

main = (putStr "Hello" >> putStr "World" >> putStr "!")

returns 'HelloWorld!' as expected. Hence I must be completely on the wrong track, which is why I came here.

Thanks for your help!

1 Answers

I think you main error is that you messed up with the handle:

main = getArgs >>= (mapM_ (\file -> (openFile file ReadMode >>= (\handle -> (hGetLine handle >>= print)  >> hClose handle)  ) ) ) 

this way you did it >> was for the (-> handle) Monad (it's a reader monad - see there is an Monad instance for (->) c for constant c) not the IO!

So it did indeed pass the handle to both hGetLine handle >>= print and hClose but >> ignored the first resulting IO action and returned the hClose one as the result to >>

Here the effect was passing the handle!

So yes in the end the only executed IO-effect was closing the file!

It's subtle and not obvious as you seldom see/think about the reader-monad instance like this.


here is this with do notation

main = do
  args <- getArgs 
  mapM_ (\file -> do
    handle <- openFile file ReadMode
    line <- hGetLine handle
    print line
    hClose handle) args

and I'd suggest switching to forM_ (from Control.Monad) for the args parameter:

main = do
  args <- getArgs 
  forM_ args (\file -> do
    handle <- openFile file ReadMode
    line <- hGetLine handle
    print line
    hClose handle)

now you should make sure you close the handle - you can use bracket from Control.Exception for this:

main = do
  args <- getArgs 
  forM_ args (\file -> do
    bracket
      (openFile file ReadMode)
      hClose
      (\h -> do
        line <- hGetLine h
        print line
      )
    )

or (as this is very common) just withFile from System.IO which does the opening/closing for you:

main = do
  args <- getArgs 
  forM_ args (\file -> do
    withFile file ReadMode
      (\h -> do
        line <- hGetLine h
        print line
      )
    )

finally you don't really have to use all the handle stuff you can use the (lazy) readFile instead and be a bit safer with empty files too:

main = do
  args <- getArgs 
  forM_ args (\file -> do
    content <- readFile file
    let ls = lines content
    case ls of
      [] -> putStrLn "no line in file"
      (firstLine:_) -> putStrLn firstLine
    )
Related