7

The following function f attempts to read an Int twice by using an IO (Maybe Int) function twice, but “short-circuits” execution after successfully reading one Int:

readInt :: IO (Maybe Int)

f :: IO (Maybe Int)
f = do
  n1 <- readInt
  case n1 of
      Just n' -> return (Just n')
      Nothing -> do
        n2 <- readInt
        case n2 of
              Just n' -> return (Just n')
              Nothing -> return Nothing

Is there a good way to refactor this code? This would get very hairy if I extended it to three attempts…

(My thought process: Seeing this “staircasing” tells me that maybe I should be using the Monad instance of Maybe, but since this is already in the IO monad, I would then have to use MaybeT(?). However, I only need one of the readInt to succeed, so the Maybe monad's behaviour of erroring out on the first Nothing would be wrong here...)

Ralph
  • 31,584
  • 38
  • 145
  • 282
beta
  • 2,380
  • 21
  • 38

4 Answers4

8

You can use MaybeT and the MonadPlus instance to use msum:

f :: MaybeT IO Int
f = msum [readInt, readInt, readInt]
Cactus
  • 27,075
  • 9
  • 69
  • 149
Lee
  • 142,018
  • 20
  • 234
  • 287
4

You need the alternative instance for MaybeT:

instance (Functor m, Monad m) => Alternative (MaybeT m) where
    empty = mzero
    (<|>) = mplus

instance (Monad m) => MonadPlus (MaybeT m) where
    mzero = MaybeT (return Nothing)
    mplus x y = MaybeT $ do
        v <- runMaybeT x
        case v of
            Nothing -> runMaybeT y
            Just _  -> return v

I.e. compute the first argument and return the value if it's Just, otherwise compute the second argument and return the value.

The code:

import Control.Applicative
import Control.Monad
import Control.Monad.Trans.Maybe
import Text.Read

readInt :: MaybeT IO Int
readInt = MaybeT $ readMaybe <$> getLine 

main = runMaybeT (readInt <|> readInt) >>= print
effectfully
  • 12,325
  • 2
  • 17
  • 40
1

First of all,

n2 <- readInt
case n2 of
      Just n' -> return (Just n')
      Nothing -> return Nothing

is really just readInt. You are picking apart a Maybe value in order to put together the same one.

For the rest, I think the most succinct way in this case is to use the maybe function. Then you can get just

f = maybe readInt (return . Just) =<< readInt
Ørjan Johansen
  • 18,119
  • 3
  • 43
  • 53
1

Another way of doing this is with the iterative monad transformer from the free package.

import Control.Monad.Trans.Iter (untilJust,retract,cutoff,IterT)

readInt :: IO (Maybe Int)
readInt = undefined

f' :: IterT IO Int
f' = untilJust readInt

f :: IO (Maybe Int)
f = (retract . cutoff 2) f'

Where cutoff specifies the maximum number of retries.

The advantage of this approach is that you can easily interleave other repeated actions with f', thanks to the MonadPlus instance of IterT. Logging actions for example, or wait actions.

danidiaz
  • 26,936
  • 4
  • 45
  • 95