Search code examples
haskelllambdasyntaxfunctional-programmingio-monad

Is h a handle or a lambda function (or both)?


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)

Solution

  • 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?