Search code examples
haskelllogical-operatorsstrictness

Logical AND strictness with IO monad


I am trying to write a simple program in Haskell. It should basically run two shell commands in parallel. Here is the code:

import System.Cmd
import System.Exit
import Control.Monad

exitCodeToBool ExitSuccess = True
exitCodeToBool (ExitFailure _) = False

run :: String -> IO Bool
run = (fmap exitCodeToBool) . system

main = liftM2 (&&) (run "foo") (run "bar")

But command "foo" returns ExitFailure and I expect "bar" never to run. This is not the case! They both run and both show errors on the console.

At the same time

False && (all (/= 0) [1..])

evaluates perfectly well; this means the second argument is not calculated. How do I do the same with system commands in my app?


Solution

  • I think using && for conditional execution is something of a bad habit. Sure it's just a matter of reason to do this for side-effect-free stuff like the False && all (/=0) [1..], but when there are side-effects it's quite confusionsome to make them dependent in such a hidden way. (Because the practise is so widespread, most programmers will immediately recognise it; but I don't think it's something we should encourage, at least not in Haskell.)

    What you want is a way to express: "execute some actions, until one yields False".

    For your simple example, I'd just do it explicitly:

    main = do
       e0 <- run "foo"
       when e0 $ run "bar"
    

    or short: run "foo" >>= (`when` run "bar").

    If you want to use this more extensively, it's good to do it in a more general manner. Simply checking a boolean condition is not very general, you'll normally also want to pass on some kind of result. Passing on results is the main reason we use a monad for IO, rather then simply lists of primitive actions.

    Aha, monads! Indeed, what you need is the IO monad, but with an extra "kill switch": either you do a sequence of actions, each possibly with some result to pass on, or – if any of them fails – you abort the entire thing. Sounds a lot like Maybe, right?

    http://www.haskell.org/hoogle/?hoogle=MaybeT

    import Control.Monad.Trans.Maybe
    
    run :: String -> MaybeT IO ()
    run s = MaybeT $ do
       e <- system s
       return $ if exitCodeToBool e then Just () else Nothing
    
    main = runMaybeT $ do
       run "foo"
       run "bar"