I'm writing a REPL in Haskell. The basic idea looks like this:
repl :: IO ()
repl = do putStr ">> "
hFlush stdout
input <- getLine
...
repl
I'd like to mimic some GHCi behaviour that when CTRL-C is pressed, the REPL discards the current line and starts a new empty line for user input. It has come to my mind that SIGINT raises UserInterrupt
of AsyncException
, which can be handled by catch
. Therefore some modifications on the input line:
repl :: IO ()
repl = do putStr ">> "
hFlush stdout
input <- getLine `catch` handleCC
repl
handleCC :: AsyncException -> IO String
handleCC _ = do putStr "\n>> "
hFlush stdout
getLine `catch` handleCC
Within handleCC
, AsyncException
will be handled once more by recursion, so surely I can interrupt the REPL infinite times, right...?
Of course I can't; a second CTRL-C still terminates the REPL. But why?
>> something^C
>> another^C
[Process exited 130]
After digging deeper, I believe I can conclude that catch
does not properly handle asynchronous exceptions, neither try
or similar. mask
seems to be a potential solution but I found its usage a bit of sophisticated even with the documentation.
This answer provides a working solution by installing a persistent signal handler using the POSIX API. Integrated with my code:
import Control.Monad (forever)
import System.IO (hFlush, stdout)
import System.Posix.Signals (Handler (Catch), installHandler, sigINT)
repl = do installHandler sigINT (Catch $ putStr "\n>> ") Nothing
forever $ do putStr ">> "
hFlush stdout
input <- getLine
...