Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Understanding `withFile` with Example

Tags:

haskell

I implemented withFile in Haskell:

withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a  
withFile' path iomode f = do
  handle <- openFile path iomode
  result <- f handle
  hClose handle
  return result

When I ran the main provided by Learn You a Haskell, it printed out the content of "girlfriend.txt," as expected:

import System.IO

main = do
    withFile' "girlfriend.txt" ReadMode (\handle -> do 
        contents <- hGetContents handle
        putStr contents)

I wasn't sure if my withFile' would've worked with the last 2 lines: (1) close the handle and (2) returning the result as anIO a.

Why didn't the following happen?

  1. result gets lazily bound to f handle
  2. hClose handle closes the file handle
  3. result gets return'd, which results in the actual evaluate of f handle. Since handle was closed, an error gets thrown.
like image 830
Kevin Meredith Avatar asked May 28 '26 21:05

Kevin Meredith


2 Answers

Lazy IO is popularly known as confusing.

It depends on whether putStr executes before hClose or not.

Notice the difference between the first and second uses (the brackets are unnecessary but clarifying in the second example).

ghci> withFile' "temp.hs" ReadMode (hGetContents >=> putStr) -- putStr 
    import System.IO
    import Control.Monad
    withFile' :: FilePath -> IOMode -> (Handle -> IO a) -> IO a  
    withFile' path iomode f = do
        handle <- openFile path iomode
        result <- f handle
        hClose handle
        return result
ghci> (withFile' "temp.hs" ReadMode hGetContents) >>= putStr
ghci>

In both cases, the f passed in gets a chance to run before the handle is closed. Because of lazy evaluation, hGetContents only reads the file if it needs to, i.e. is forced to in order to produce output for some other function.

In the first example, since f is (hGetContents >=> putStr), the full contents of the file must be read in order to execute putStr.

In the second example, nothing needs to be evaluated after hGetContents in order to return result, which is a lazy list. (I can quite happily return (show [1..]) which will only fail to terminate if I choose to use the entire output.) This is seen as a problem for lazy IO, which is fixed by alternatives such as strict IO, pipes or conduit.

Maybe returning the empty string for a file when the handle was closed prematurely is a bug, but certainly running the entirety of f before closing it is not.

like image 161
AndrewC Avatar answered Jun 01 '26 02:06

AndrewC


Equational reasoning means that you can reason about Haskell code by just inlining and substituting things (with certain caveats, but they don't apply here).

This means that all I need to do to understand your code is to take the withFile' here:

import System.IO

main = do
    withFile' "girlfriend.txt" ReadMode (\handle -> do 
        contents <- hGetContents handle
        putStr contents)

... and inline its definition:

main = do
    handle   <- openFile "girlfriend.txt" ReadMode
    contents <- hGetContents handle
    result   <- putStr contents
    hClose handle
    return result

Once you inline its definition, it's easier to see what is going on. putStr evaluates the entire contents of the file before you close the handle, so there is no error. Also, result is not what you think it is: it's the return value of putStr, which is just (), not the contents of the file.

like image 40
Gabriella Gonzalez Avatar answered Jun 01 '26 02:06

Gabriella Gonzalez



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!