Hew Wolff
Hew Wolff

Reputation: 1509

Haskell key input memory leak

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

Answers (2)

Hew Wolff
Hew Wolff

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

luqui
luqui

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

Related Questions