Skip to content

Explicit "early return" functions? #112

@ChickenProp

Description

@ChickenProp

tl;dr: I suggest exposing the functions

runBail :: Except a a -> a
runBail = runIdentity . runBailT

runBailT :: ExceptT a m a -> m a
runBailT act = either id id <$> runExceptT act

bail :: a -> ExceptT a m x
bail = throwE

from Control.Monad.Except, or from some new module.


One of the things I use ExceptT for is adding an early-return to avoid adding indent levels. Something like

lookupDomain :: URL -> IO (Either NoSuchDomain IpAddr)
lookupDomain url = fmap (either id id) $ runExceptT $ do
  check1 <- lookupInLocalCache url
  maybe (pure ()) throwE check1

  check2 <- lookupInHostFile url
  maybe (pure ()) throwE check2

  check3 <- lookupThroughDNS url
  maybe (pure ()) throwE check3

  pure $ Left NoSuchDomain

in place of

lookupDomain :: URL -> IO (Either NoSuchDomain IpAddr)
lookupDomain url = do
  check1 <- lookupInLocalCache url
  case check1 of
    Just (eResult) -> pure eResult
    Nothing -> do
      check2 <- lookupInHostFile url
      case check2 of
        Just (eResult) -> pure eResult
        Nothing -> do
          check3 <- lookupThroughDNS url
          case check3 of
            Just (eResult) -> pure eResult
            Nothing -> pure $ Left NoSuchDomain

It's fine, but doesn't feel quite fit for purpose. In particular using throwE for something that isn't an error feels a bit off. I'll often add a comment saying "using throwE as an early-return" or similar, to make it clear what's going on.

Could there be something specifically intended for early-return?

One possibility would be to expose the functions

runBail :: Except a a -> a
runBail = runIdentity . runBailT

runBailT :: ExceptT a m a -> m a
runBailT act = either id id <$> runExceptT act

bail :: a -> ExceptT a m x
bail = throwE

from Control.Monad.Except, or from somewhere else. Then the first example doesn't change much:

lookupDomain :: URL -> IO (Either NoSuchDomain IpAddr)
lookupDomain url = runBailT $ do
  check1 <- lookupInLocalCache url
  maybe (pure ()) bail check1

  check2 <- lookupInHostFile url
  maybe (pure ()) bail check2

  check3 <- lookupThroughDNS url
  maybe (pure ()) bail check3

  pure $ Left NoSuchDomain

But IMO it's a bit clearer. And when newcomers ask "how do I return early from a function", there are dedicated names we can point them at.

The awkward things here are that there's no corresponding BailT transformer, and that it's still possible to use throwE and (worse) catchE. Still, on balance I think I'd like this. It's not a big deal but I think it would be a slight improvement.

It would also be possible to have a BailT transformer, but it would be exactly the same as ExceptT. You can't remove the separate type parameter for the error, because e.g. you need maybe (pure ()) bail check1 :: ExceptT e m (). You could have type BailT m a = ExceptT a m a, but then it can't be used everywhere. And of course, an extra transformer means a bunch more instances to write. So I like this less.

Maybe there's something else that could work here?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions