Reputation: 1509
I came up with the following code for the raw-input problem, as discussed in SO Haskell read raw keyboard input. Unfortunately, when I go to ghci, run getAllInput
, and hit the right arrow key, it never returns. Unless I kill it pretty quickly, it seems to eat all my memory so that other applications stop responding and I have to restart the OS. In the Activity Monitor I can see the memory for the ghc process go quickly into the gigabytes.
(1) I think the problem is in the recursive call of go
, which is evaluated lazily by calling hReady
before getChar
; this means hReady
keeps returning true and the stack grows forever. Is that plausible?
(2) I'm used to languages in which this would soon cause a stack overflow exception, so it wouldn't prevent me from working. Is there any general way to protect against this massive memory leak? Maybe starting ghci with a hard limit on memory use?
import System.IO
-- For example, should get "\ESC[C" from the user hitting the right arrow key.
getAllInput :: IO [Char]
getAllInput =
let
go :: IO [Char] -> IO [Char]
go chars = do
more <- hReady stdin
if more then go (added chars getChar) else chars
added :: IO [Char] -> IO Char -> IO [Char]
added chars char = do
chars1 <- chars
char1 <- char
return (chars1 ++ [char1])
in do
hSetBuffering stdin NoBuffering
firstChar <- getChar
go (return [firstChar])
I'm running ghci 7.10.3 in OS X 10.11.6. I cleaned up the code in some obvious ways, basically following the similar SO answer: putting the getChar
call in its own line fixes the problem. But I'd like to understand this better in case it bites me again.
Upvotes: 2
Views: 262
Reputation: 1509
Summarizing the discussion...
(1) As explained by luqui's answer, the memory leak is caused by an infinite loop in which lazy evaluation prevents getChar
from being called. Adding a line like c <- getChar
forces the call to happen, so that go (added chars c)
is now safe to call.
(2) Starting GHCi with a limited heap size, as in ghci getchars.hs +RTS -M100m
, will interrupt the memory leak before it eats up all memory. See the GHC Users Guide for further details.
Upvotes: 1
Reputation: 60463
You have an infinite loop in go
. If hReady
returns true, then you call go
again, which immediately calls hReady
again, which will of course return true, and so on. You probably assume that added
will be run because of go (added chars getChar)
, but it will not; it is just building an IO
action and passing it to go
as an argument, but that argument is only used when hReady
returns False
. The name chars
is misleading -- chars
is actually an I/O procedure which will return a list of characters when it is eventually run.
In general, when using monads, a "normal" function signature looks like:
:: Foo -> Bar -> Baz -> IO Quuz
That is, the monad (IO
) is only present on the return value, not the arguments. A signature like
:: IO Foo -> IO Bar
usually indicates that something higher-order is going on, for example that this function will perhaps execute its argument multiple times or in a new context.
I recommend the signatures
go :: [Char] -> IO [Char]
added :: [Char] -> Char -> IO [Char]
and trying to get the program to compile from there.
You should also try changing added
to be a pure function
added :: [Char] -> Char -> [Char]
because it doesn't actually have any side effects. The implementation and usage will need to be altered a little, though.
Upvotes: 5