0

I am looking at a simple IO program from the Haskell Wikibook. The construction presented on that page works just fine, but I'm trying to understand "how".

The writeChar function below takes a filepath (as a string) and a character, and it writes the character to the file at the given path. The function uses a bracket to ensure that the file opens and closes properly. Of the three computations run in the bracket, the "computation to run in-between"---as I understand it---is a lambda function that returns the result of hPutChar h c.

Now, hPutChar itself has a declaration of hPutChar :: Handle -> Char -> IO (). This is where I'm lost. I seem to be passing h as the handle to hPutChar. I would expect a handle somehow to reference the file opened as fp, but instead it appears to be recursively calling the lambda function \h. I don't see how this lambda function calling itself recursively knows to write c to the file at fp.

I would like to understand why the last line of this function shouldn't read (\h -> hPutChar fp c). Attempting to run it that way results in "Couldn't match type ‘[Char]’ with ‘Handle’" which I consider sensible given that hPutChar expects a Handle datatype as opposed to a string.

import Control.Exception
writeChar :: FilePath -> Char -> IO ()
writeChar fp c =
    bracket
      (openFile fp WriteMode)
      hClose
      (\h -> hPutChar h c)
Will Ness
  • 70,110
  • 9
  • 98
  • 181
mttpgn
  • 327
  • 6
  • 17
  • 2
    The `fp` is a FilePath (which is really just a string). The `h` in the lambda expression is the `Handle` that is inside the IO monad that `openFile` returns. See http://hackage.haskell.org/package/base-4.12.0.0/docs/System-IO.html. – Marc Talbot Apr 15 '19 at 01:25
  • `\h` is not really an entity in itself; it is merely a part of the lambda function `\h -> hPutChar fp c`. – duplode Apr 15 '19 at 01:32
  • I misled myself thinking that the `h` and `\h` on the same line referred to the same entity! – mttpgn Apr 15 '19 at 01:52
  • the importance of staying engaged and talking in the comments: thanks to your above comment your misunderstanding (that "`\h`" is an entity in itself) finally became clear and could be addressed! / ++ – Will Ness Apr 15 '19 at 14:31

3 Answers3

4

Let's have a look at the type of bracket (quoted as it appears in your Haskell Wiki link):

bracket :: IO a        -- computation to run first ("acquire resource")
        -> (a -> IO b) -- computation to run last ("release resource")
        -> (a -> IO c) -- computation to run in-between
        -> IO c

In your use case, the first argument, openFile fp WriteMode is an IO Handle value, a computation that produces a handle corresponding to the fp path. The third argument, \h -> hPutChar h c, is a function that takes a handle and returns a computation that writes to it. The idea is that the function you pass as the third argument specifies how the resource produced by the first argument will be used.

duplode
  • 33,731
  • 7
  • 79
  • 150
3

There is no recursion going on here. h is indeed a Handle. If you've programmed in C, the rough equivalent is a FILE. The handle consists of a file descriptor, buffers, and whatever else is needed to perform I/O on the attached file/pipe/terminal/whatever. openFile takes a path, opens the requested file (or device), and provides a handle that you can use to manipulate the requested file.

bracket
  (openFile fp WriteMode)
  hClose
  (\h -> hPutChar h c)

This opens the file to produce a handle. That handle is passed to the third function, which binds it to h and passes it to hPutChar to output a character. Then in the end, bracket passes the handle to hClose to close the file.

If exceptions didn't exist, you could implement bracket like this:

bracket
  :: IO resource
  -> (resource -> IO x)
  -> (resource -> IO a)
  -> IO a
bracket first last middle = do
  resource <- first
  result <- middle resource
  last resource
  pure result

But bracket actually has to install an exception handler to endure that last is called even if an exception occurs.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
2
hPutChar :: Handle -> Char -> IO ()

is a pure Haskell function, which, given two arguments h :: Handle and c :: Char, produces a pure Haskell value of type IO (), an "IO action":

         h :: Handle      c :: Char
---------------------------------------------
hPutChr  h                c          :: IO ()

This "action" is simply a Haskell value, but when it appears inside the IO monad do block under main, it becomes executed by the Haskell run-time system and then it actually performs the I/O operation of putting a character c into a filesystem's entity referred to by the handle h.

As for the lambda function, the actual unambiguous syntax is

(\ h -> ... ) 

where the white space between \ and h is optional, and the whole (.......) expression is the lambda expression. So there is no "\h entity":

  • (\ ... -> ... ) is the lambda-expression syntax;
  • h in \ h -> is the lambda function's parameter,
  • ... in (\ h -> ... ) is the lambda function's body.

bracket calls the (\h -> hPutChar h c) lambda function with the result produced by (openFile fp WriteMode) I/O computation, which is the handle of the file name referred to by fp, opened according to the mode WriteMode.

Main thing to understand about the Haskell monadic IO is that "computation" is not a function: it is the actual (here, I/O) computation performing the actual file open -- which produces the handle -- which is then used by the run-time system to call the pure Haskell function (\ h -> ...) with it.

This layering (of pure Haskell "world" and the impure I/O "world") is the essence of .... yes, Monad. An I/O computation does something, finds some value, uses it to call a pure Haskell function, which creates a new computation, which is then run, feeds its results into the next pure function, etc. etc.

So we keep our purity in Haskell by only talking about the impure stuff. Talking is not doing.

Or is it?

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 1
    "h in \ h -> is the lambda function's parameter" -- That's what I was missing, as well as the understanding that `bracket` fed the result of its first computation into it's in-between computation (which @dfeuer summarized quite well). I was incorrectly reading the anonymous function's parameter as the anonymous function's "name". – mttpgn Apr 15 '19 at 13:53