How to terminate a computation that runs in the `IO` monad?

Question

There is a library that provides a data type F and a function of type

ffoldlIO :: (b -> a -> IO b) -> b -> F a -> IO b

The function is similar to

foldlIO :: (b -> a -> IO b) -> b -> [a] -> IO b
foldlIO f a = \xs -> foldr (\x r (!a') -> f a' x >>= r) return xs a

I wonder whether foldlIO (and thus ffoldlIO) can run in a short-circuit fashion.

Consider this example:

example1 :: IO Int
example1 = foldlIO (\a x -> if a < 4 then return (a + x) else return a) 0 [1..5]

Here foldlIO traverses the entire list, but what if we throw an exception to stop the computation and then catch it? Something like this:

data Terminate = Terminate
  deriving (Show)

instance Exception Terminate

example2 :: IO Int
example2 = do
  ra <- newIORef 0
  let step a x
        | a' < 4    = return a'
        | otherwise = writeIORef ra a' >> throwIO Terminate
        where a' = a + x
  foldlIO step 0 [1..] `catch` \(_ :: Terminate) -> readIORef ra

Is this reliable? Is there a better way to terminate a computation that runs in the IO monad (and no other monad) or am I not supposed to do this at all?


Show source
| haskell   | monads   | exception   | io   | short-circuiting   2016-12-13 14:12 2 Answers

Answers to How to terminate a computation that runs in the `IO` monad? ( 2 )

  1. 2016-12-13 15:12

    For example, you can use ContT monad transformer like this:

    example3 :: IO Int
    example3 = flip runContT return . callCC $ \exit -> do
        let step a x
                | a' < 4    = return a'
                | otherwise = exit a'
                where a' = a + x
        foldM step 0 [1..]
    

    Also, you can define you own version of foldM with posibility of termination.

    termFoldM :: (Monad m, Foldable t) =>
        ((b -> ContT b m c) -> b -> a -> ContT b m b) -> b -> t a -> m b
    termFoldM f a t = flip runContT return . callCC $ \exit -> foldM (f exit) a xs
    
    example4 :: IO Int
    example4 = termFoldM step 0 [1..]
      where
        step exit a x
            | a' < 4    = return a'
            | otherwise = exit a'
            where a' = a + x
    

    But this way (with ContT) has one problem. You can't easy do some IO actions. For example, this code will not be compiled, because step function must return value of type ContT Int IO Int not IO Int.

    let step a x
            | a' < 4    = putStrLn ("'a = " ++ show a') >> return a'
            | otherwise = exit a'
            where a' = a + x
    

    Fortunately, you can solve this by the lift function, like this:

    let step a x
            | a' < 4    = lift (putStrLn ("'a = " ++ show a')) >> return a'
            | otherwise = exit a'
            where a' = a + x
    
  2. 2016-12-13 17:12

    My first answer was not correct. So, I'll try to improve.


    I think that the use of exceptions to terminate in IO monad is not a hack but it does not look clean. I propose to define the instance MonadCont IO like this:

    data Terminate = forall a . Terminate a deriving (Typeable)
    instance Show Terminate where show = const "Terminate"
    instance Exception Terminate
    
    instance MonadCont IO where
        callCC f = f exit `catch` (\(Terminate x) -> return . unsafeCoerce $ x)
          where exit = throwIO . Terminate
    

    Then you can rewrite your example more cleaner.

    example :: IO Int
    example = callCC $ \exit -> do
        let step a x
                | a' < 4    = return a'
                | otherwise = exit a'
                where a' = a + x
        foldlIO step 0 [1..]
    

    Variant with IOREf.

    data Terminate = Terminate deriving (Show, Typeable)
    instance Exception Terminate
    
    instance MonadCont IO where
        callCC f = do
            ref <- newIORef undefined
            let exit a = writeIORef ref a >> throwIO Terminate
            f exit `catch` (\Terminate -> readIORef ref)
    

Leave a reply to - How to terminate a computation that runs in the `IO` monad?

◀ Go back