Explain GHC.Internal.IO.Handle.Text.hPutStr'

I am looking at hPutStr’ and I do not understand why buffer_mode and nl are returned from the wantWritableHandle action. It seems strange to me that the case statement is not just in the action function. Is there any good reason for this, is it just the preference of the author to keep h_ out of scope with the case statement, or something else?

is it just the preference of the author to keep h_ out of scope

I guess so. Bear in mind that this code was probably written years ago and hardly revisited since, so it’s not necessarily written in any coherent style.

1 Like

It is definitely not just preference nor scope. hPutChar, and commitBuffer both call wantWritableHandle and will deadlock if called within wantWritableHandle in hPutStr'.

Note that wantWritableHandle h act ultimately calls withHandle' to run its handler act in a masked context:

withHandle' fun h m act =
 mask_ $ do
   (h',v)  <- do_operation fun h act m
   checkHandleInvariants h'
   putMVar m h'
   return v

mask_ makes that the block cannot be interrupted by asynchronous exceptions (perhaps thrown from another thread). Although it may be vital to perform cleanup work as part of the masked block, it is important to keep masked blocks as small as possible, in order to handle asynchronous exceptions as promptly as possible.

Hence it is good that the act in hPutStr' only does a single, fast thing: obtaining a spare buffer. It should not also write to the file handle, because that may block indefinitely, for example when there is no buffering and we are writing to a pipe. That would mean that killing your app with Ctrl+C would not work unless you read out all contents of the pipe first.

1 Like

But there’s also a more specific reason it won’t work, right? That is, a Handle is a Handle__ in an MVar, and withHandle' takes that MVar and only returns it once its body has finished. Therefore you can’t nest calls to withHandle'.