L. Catallo
L. Catallo

Reputation: 563

How can I perpetually retry an IO until I get a result?

I'm trying to read an integer from a command line prompt and I want my program keep asking for input until it can parse a proper value.

This is what I came up with

import Control.Exception
import System.IO

prompt :: String -> IO String
prompt text = do
  putStr text
  hFlush stdout
  getLine

getInt :: IO Int
getInt = handle recoverError readParse
  where recoverError :: SomeException -> IO Int
        recoverError _ = getInt
        readParse = fmap read $ prompt ">> "

main :: IO ()
main = fmap show getInt >>= putStrLn

I expected the handle function to recursively call getInt any time an Exception from read arises, but apparently that's not the case.

This is what I see when I execute this program

$ ./main
>> 10
10

$ ./main
>> not a number
main: Prelude.read: no parse

I'm new to haskell so probably missing something obvious here.

Any help is appreciated.

Upvotes: 1

Views: 198

Answers (2)

Bergi
Bergi

Reputation: 664548

The problem is that the exception from read does not arise from readParse due to Haskell's laziness. The read result is not evaluated until the shown string is output by putStrLn, and only then the exception happens.

You could probably work around this by using evaluate, but the proper solution here is not to rely on exceptions at all:

read fails with an error if the parse is unsuccessful, and it is therefore discouraged from being used in real applications. Use readMaybe or readEither for safe alternatives.

getInt :: IO Int
getInt = maybe getInt return =<< readMaybe <$> prompt ">> "
-- alternatively written as
getInt = do
  input <- prompt ">> "
  case readMaybe input of
    Just v  -> return v
    Nothing -> getInt

Upvotes: 4

Daniel Wagner
Daniel Wagner

Reputation: 152837

Use readIO instead of read, as in:

readParse = prompt ">> " >>= readIO

The difference is that fmap read creates an IO action that always succeeds and produces a pure result with an exception embedded in it, while readIO creates an IO action that may throw an IO exception but whose pure value result always succeeds. You may also combine getLine and readIO, using readLn instead, as in:

prompt :: Read a => String -> IO a
prompt text = do
  ...
  readLn

Upvotes: 4

Related Questions