Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to "escape early" in a web monad

Tags:

haskell

Something that happens to me a lot while web programming: I want to run an operation that has a chance of failure. On a failure, I want to send the client a 500. Normally though, I just want to continue executing a series of steps.

doSomeWebStuff :: SomeWebMonad ()
doSomeWebStuff = do
    res <- databaseCall
    case res of 
        Left err -> status 500
        Right val -> do
             res2 <- anotherDatabaseCall (someprop val)
             case res2 of 
                 Left err -> status 500
                 Right val2 -> text $ show val2

since the errs are exceptions, I don't like that I need all that case stuff just to catch them. I want to do the same thing whenever anything is a left. Is there a way to express that on one line with something like guard, but control what it returns on an exit?

In another language I could do this:

function doSomeWebStuff() {
    var res = databaseCall()
    if (res == Error) return status 500
    var res2 = anotherDatabaseCall(res.someprop)
    if (res2 == Error) return status 500
    return text(res2)
}

So, I'm ok writing some boilerplate, but I don't want the errors to mess with my nesting, when it's far more common to just want to continue forward with the found case.

What's the cleanest way to do this? I know in theory I can use a monad to exit early on a failure, but I've only seen examples with Maybe and it would return Nothing at the end, rather than letting me specify what it returns.

like image 340
Sean Clark Hess Avatar asked Jan 27 '12 13:01

Sean Clark Hess


2 Answers

Here's how I would do it with ErrorT. Disclaimer: I have never actually used ErrorT before.

webStuffOr500 :: ErrorT String SomeWebMonad () -> SomeWebMonad ()
webStuffOr500 action = do
  res <- runErrorT action
  case res of
    Left err -> do
      logError err -- you probably want to know what went wrong
      status 500
    Right () -> return ()

doSomeWebStuff :: SomeWebMonad ()
doSomeWebStuff = webStuffOr500 doSomeWebStuff'

doSomeWebStuff' :: ErrorT String SomeWebMonad ()
doSomeWebStuff' = do
    val <- ErrorT databaseCall
    val2 <- ErrorT $ anotherDatabaseCall (someprop val)
    lift $ text $ show val2

Here are the imports and type declarations I used to make sure it all typechecks correctly:

import Control.Monad.Identity
import Control.Monad.Error
import Control.Monad.Trans (lift)
import Control.Monad

type SomeWebMonad = Identity

data Foo = Foo
data Bar = Bar
data Baz = Baz deriving (Show)

someprop :: Foo -> Bar
someprop = undefined
databaseCall :: SomeWebMonad (Either String Foo)
databaseCall = undefined
anotherDatabaseCall :: Bar -> SomeWebMonad (Either String Baz)
anotherDatabaseCall = undefined
logError :: String -> SomeWebMonad ()
logError = undefined
text :: String -> SomeWebMonad ()
text = undefined
status :: Int -> SomeWebMonad ()
status = undefined

If I'm doing this all wrong then please, somebody shout out. It may be wise, if you take this approach, to modify the type signature of databaseCall and anotherDatabaseCall to also use ErrorT, that way a <- ErrorT b can be reduced to a <- b in doSomeWebStuff'.

Since I'm a complete noob at ErrorT, I can't really do any hand-holding besides "here's some code, go have some fun".

like image 71
Dan Burton Avatar answered Oct 02 '22 23:10

Dan Burton


Not a direct answer to your question, but have you considered using Snap? In snap, we have short-circuiting behavior built-in with an idiomatic:

getResponse >>= finishWith

where

finishWith ::  MonadSnap m => Response -> m a

So given a response object, it will terminate early (and match whatever type comes after that). Haskell laziness will ensure computations within Snap monad after finishWith won't be executed.

I sometimes make a little helper:

finishEarly code str = do
  modifyResponse $ setResponseStatus code str
  modifyResponse $ addHeader "Content-Type" "text/plain"
  writeBS str
  getResponse >>= finishWith

which I can then use anywhere in my handlers.

myHandler = do
  x <- doSomething
  when (x == blah) $ finishEarly 400 "That doesn't work!!"
  doOtherStuff
like image 40
ozataman Avatar answered Oct 03 '22 00:10

ozataman