Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Composing ExitCodes in Turtle. Why is there no Monad/Monad Transformer instance?

I am writing a shell script in Haskell using turtle and would like to know best practices on composing commands that could fail.

Now I have a case expression staircase, like so:

runRemote :: MonadIO io => Text -> Text -> io ()
runRemote oldVersion' newVersion' = sh $ do
  mkdir "out"
  e1 <- shell ("command " <> oldVersion') empty
  case e1 of
    ExitFailure n -> cleanup 
    ExitSuccess -> do
      e2 <- shell ("command " <> newVersion') empty
      case e2 of
        ExitFailure n -> cleanup 
        ExitSuccess -> do
          curDir <- pwd
          cd (curDir <.> oldVersion')
          e3 <- shell ("command something else") empty
          case e3 of
           -- ...
           -- And so on...

If the case expression was expanding on a Maybe type, the solution would be to derive a Monad instance.

Is there a special reason the library author didn't already derive a Monad instance for ExitCode or is there a better way to do error handling for Haskell shell code?

like image 297
DJG Avatar asked Oct 19 '15 23:10

DJG


3 Answers

One alternative is using (.&&.) and (.||.) from Turtle.Prelude.

(.&&.) :: Monad m => m ExitCode -> m ExitCode -> m ExitCode

Analogous to && in Bash

Runs the second command only if the first one returns ExitSuccess

(.||.) :: Monad m => m ExitCode -> m ExitCode -> m ExitCode

Analogous to || in Bash

Run the second command only if the first one returns ExitFailure

They allow you to chain your commands like this (note that everything involved must return an ExitCode, including the cleanup):

(command1 .&&. command2) .||. cleanup

Or, if you need different cleanup actions in each case:

(command1 .||. cleanup1) .&&. (command2 .||. cleanup2)

By the way, it is worth noting that ExitCode is not defined by turtle but rather by base, in the System.Exit module.

like image 141
duplode Avatar answered Nov 08 '22 22:11

duplode


ExitCode is not a monad, and is not a monad transformer. A monad needs to take a type argument, and a monad transformer needs to take two. ExitCode takes none. Now suppose we ignore that not-so-little problem for a bit. Can you come up with a meaningful interpretation of

join :: ExitCode (ExitCode a) -> ExitCode a

Yeah, I can't either. You could argue reasonably that shell should instead produce Either FailureCode (), or perhaps work in ExceptT FailureCode IO, but the library authors may have thought that too confusing or inflexible for the job.

like image 20
dfeuer Avatar answered Nov 08 '22 22:11

dfeuer


You can use MaybeT to avoid staircasing this way:

{-# LANGUAGE OverloadedStrings #-}

import Turtle
import Control.Monad.Trans
import Control.Monad.Trans.Maybe

check io = do ec <- lift io
              MaybeT $ case ec of
                         ExitSuccess -> return (Just True)
                         _           -> return Nothing

checkShell a b = check (shell a b)

main = do
  dostuff
  putStrLn "cleaning up"

dostuff = runMaybeT $ do
  checkShell "date" empty
  checkShell "/usr/bin/false" empty
  checkShell "pwd" empty
like image 1
ErikR Avatar answered Nov 08 '22 21:11

ErikR