Combine `StateT` and `InputT` to maintain state on Ctrl-C interrupt

Hi, I'm stuck with maintaining a State in a Haskeline program that would survive an interrupt with Ctrl-C. The following is a MWE I have distilled out of a bigger project. It revolves around a simple (I guess) read-eval-print loop using Haskeline, and a stacked StateT to maintain a state. This MWE reads a line from the user, calculates its `length`, and adds the length to an Int forming the state. If the user types the special input "sleep" the program sleeps for 5 seconds, simulating a longer computation. Every time the user presses Ctrl-C * a running computation should be interrupted, or * when at the prompt, the current input should be cleared. * In any case, the state must be maintained! Unfortunately, Ctrl-C also clears the state. At first I thought I had stacked the `StateT` and `InputT` in the wrong order. But changing from `InputT (StateT Int IO) ()` to `StateT Int (InputT IO) ()` seems not to change anything (which really gives me the creeps). This message contains both versions (one commented out), they compile with $ ghc --version The Glorious Glasgow Haskell Compilation System, version 7.10.1 $ ghc --make -Wall -outputdir tmp -o mwe StateMwe.lhs Example run, comments (`<--...`) added later: $ ./mwe type> arglschluargl Adding length of "arglschluargl" 13 type> lalala Adding length of "lalala" 19 type> <--no input, state unchanged Adding length of "" 19 type> sleep Sleeping 5 seconds... ^C <--Here I have hit C-c type> <--no input, state unchanged Adding length of "" 0 <--WRONG: that should be 19 Here we go:
import Control.Monad.State.Strict import Control.Concurrent ( threadDelay ) import System.Console.Haskeline
{-
Version one: `StateT` inside `InputT`
main :: IO () main = evalStateT (runInputT defaultSettings $ noesc repl) 0
This catches an interrupt via Ctrl-C and restarts the passed operation.
noesc :: MonadException m => InputT m a -> InputT m a noesc w = withInterrupt $ let loop = handle (\Interrupt -> loop) w in loop
The read-eval-print loop: EOF terminates, `sleep` delays, and any other input modifies the integer state.
repl :: InputT (StateT Int IO) () repl = do x <- getInputLine "\ntype> " case x of Nothing -> return () Just "sleep" -> do outputStrLn "Sleeping 5 seconds..." lift . lift . threadDelay $ 5 * 10^(6::Int) outputStrLn "...not interrupted" repl Just t -> do outputStrLn $ "Adding length of " ++ show t lift $ modify (+ length t) v <- lift get outputStrLn $ show v repl
-}
{-
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
This catches an interrupt via Ctrl-C and restarts the passed operation. I suspect that I have to rearrange this to accommodate the modified stacking, but I could not come up with anything that compiles...
noesc :: MonadException m => InputT m a -> InputT m a noesc w = withInterrupt $ let loop = handle (\Interrupt -> loop) w in loop
As above, with `lift` in different places. To get a better understanding of what's going on, I do not want to use mtl's lift-to-the-right-monad magic (yet).
repl :: StateT Int (InputT IO) () repl = do x <- lift $ getInputLine "\ntype> " case x of Nothing -> return () Just "sleep" -> do lift $ outputStrLn "Sleeping 5 seconds..." lift . lift . threadDelay $ 5 * 10^(6::Int) lift $ outputStrLn "...not interrupted" repl Just t -> do lift . outputStrLn $ "Adding length of " ++ show t modify (+ length t) v <- get lift . outputStrLn $ show v repl
-}
Any help would be welcome... Stefan -- http://stefan-klinger.de o/X /\/ \

On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
At first I thought I had stacked the `StateT` and `InputT` in the wrong order. But changing from `InputT (StateT Int IO) ()` to `StateT Int (InputT IO) ()` seems not to change anything (which really gives me the creeps).
InputT is essentially ReaderT http://hackage.haskell.org/package/haskeline-0.7.2.1/docs/src/System-Console... so it's not surprising that the order has no effect. Reader commutes with State. Tom

Tom Ellis
On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
At first I thought I had stacked the `StateT` and `InputT` in the wrong order. But changing from `InputT (StateT Int IO) ()` to `StateT Int (InputT IO) ()` seems not to change anything (which really gives me the creeps).
InputT is essentially ReaderT
http://hackage.haskell.org/package/haskeline-0.7.2.1/docs/src/System-Console...
so it's not surprising that the order has no effect. Reader commutes with State.
Note in the source there that the state of the Haskeline stuff itself uses "ReaderT (IO _) vs StateT so that exceptions (e.g. ctrl-c) don't cause us to lose the existing state." If there was a more elegant solution, Haskeline itself could use it, I guess? So it's probably at least hard to do generally for any exception occurring in the application? But could one get away with using `handle` at the particular sites where one expects to be interrupted, e.g. around the call to `threadDelay`?

On 2015-Aug-12, mikael.brockman@gmail.com wrote with possible deletions:
But could one get away with using `handle` at the particular sites where one expects to be interrupted, e.g. around the call to `threadDelay`?
Yeah, I've had that, but I don't like it: When the user's wetware decides that somethig takes longer than expected, it decides to press Ctrl-C. Now the calculation might just terminate an instant before the keypress, yielding a race condition between whether the interrupt is caught, or not (or by which exception handler). Also, pressing C-c (un)intentionally at the prompt would either terminate the program, or reset all settings made in the interactive session, thats way more than I'd like to happen. But thanks for the idea... Stefan -- http://stefan-klinger.de o/X /\/ \

haskell@stefan-klinger.de writes:
On 2015-Aug-12, mikael.brockman@gmail.com wrote with possible deletions:
But could one get away with using `handle` at the particular sites where one expects to be interrupted, e.g. around the call to `threadDelay`?
Yeah, I've had that, but I don't like it: When the user's wetware decides that somethig takes longer than expected, it decides to press Ctrl-C. Now the calculation might just terminate an instant before the keypress, yielding a race condition between whether the interrupt is caught, or not (or by which exception handler). Also, pressing C-c (un)intentionally at the prompt would either terminate the program, or reset all settings made in the interactive session, thats way more than I'd like to happen.
But thanks for the idea... Stefan
Maybe keeping the state in an I/O variable is the most viable way. I can't explain clearly why that would be the case, but it seems tricky to work "externally" with the state carried by StateT, since it only exists in the transient form of a value threaded through lambdas...

On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
Instead of this, how about `hoist`ing noesc inside the StateT? (I would have tried this myself but it would be too fiddly to reconstitute working code from your email.) Tom

On Wed, Aug 12, 2015 at 04:15:33PM +0100, Tom Ellis wrote:
On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
Instead of this, how about `hoist`ing noesc inside the StateT?
hoist is here: http://haddock.stackage.org/lts-2.22/mmorph-1.0.4/Control-Monad-Morph.html#v...

On 2015-Aug-12, Tom Ellis wrote with possible deletions:
On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
Instead of this, how about `hoist`ing noesc inside the StateT?
Hmmmm... like so?
main = runInputT defaultSettings $ evalStateT (hoist noesc repl) 0
that behaves just the same way. I need to do some reading before I understand what that actually does...
(I would have tried this myself but it would be too fiddly to reconstitute working code from your email.)
Sorry, I have taken extra care that my original posting actually compiles. I just save the message as `StateMwe.lhs` and compile... But thanks for the suggestion! Stefan -- http://stefan-klinger.de o/X /\/ \

On Wed, Aug 12, 2015 at 05:49:10PM +0200, haskell@stefan-klinger.de wrote:
On 2015-Aug-12, Tom Ellis wrote with possible deletions:
On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
Instead of this, how about `hoist`ing noesc inside the StateT?
Hmmmm... like so?
main = runInputT defaultSettings $ evalStateT (hoist noesc repl) 0
That was what I was thinking.
that behaves just the same way. I need to do some reading before I understand what that actually does...
I think it's probably a dead end then. In fact it may well be doing exactly what you were doing before in longhand.
(I would have tried this myself but it would be too fiddly to reconstitute working code from your email.)
Sorry, I have taken extra care that my original posting actually compiles. I just save the message as `StateMwe.lhs` and compile...
Good idea. I didn't think of lhs. My fault, not yours! Tom

On Wed, Aug 12, 2015 at 04:15:33PM +0100, Tom Ellis wrote:
On Wed, Aug 12, 2015 at 04:26:59PM +0200, haskell@stefan-klinger.de wrote:
Version two: `InputT` inside `StateT`
main :: IO () main = runInputT defaultSettings . noesc $ evalStateT repl 0
Instead of this, how about `hoist`ing noesc inside the StateT?
Actually, thinking about it some more I think this is a more fiddly problem that the like of http://hackage.haskell.org/package/MonadCatchIO-transformers-0.3.0.0/docs/Co... exist to solve, but I'm not so familiar with that kind of thing myself. Tom
participants (3)
-
haskell@stefan-klinger.de
-
mikael.brockman@gmail.com
-
Tom Ellis