How to refactor this code combining IO and Either?

One could do something like the following:

doStuff :: IO ()
doStuff = do
    let eThrowP = either (throw . ParseException) pure
    f <- readFile "..."
    parsed <- eThrowP $ parseFile f
    parsed' <- eThrowP . parseFile2 =<< readFile2 (getFilename parsed)
    ...

Note how similar it is to the ExceptT solution except the exceptions will just bubble up wherever.

Yep, interesting… so according to the author what has been suggested before (and I ended up implementing) is an anti pattern.

I mean, probably it doesn’t matter too much for a small program like mine, but definitely interesting stuff. At some point I want to revisit this thread, go through all the info again and see if I can make it better.

I’m not sure if this will help your learning or not but this is mostly a feature of Haskell being powerful enough to implement what are normally language level features as libraries. So you can fully commit to a personal style or learn the pros and cons of a handful. I do think that ExceptT is the right place to start for learning.

Coming back to Haskell after a while I prefer the current state over the period where the advice felt like you had to craft the perfect transformer stack before even writing anything. Strict prescriptions can win some mindshare but they’re just not for me.

I’m not a big fan of Monad transformers (other than StateT s/ReaderT r m):

handleMakePlaylist :: MakePlaylistOptions -> IO ()
handleMakePlaylist opts =
  do -- In the IO monad, so values to the left of the <- are the a in the IO a that's returned
    eLib <- readLibFromFile_ opts.libraryPath -- IO Either err library
    -- ^ Either err library    
    let ePlist = do lib' <- eLib 
                    mkPlaylist lib' opts.firstTrack opts.maxTracks
    -- ^ Either err Playlist
    either print (\plist' -> putStr $ renderResolvedPlaylist plist') ePlist
    -- either :: (err -> c) -> (a -> c) -> Either err a -> c
    -- in this case (String -> IO ()) -> (Playlist -> IO ()) -> Either String Playlist -> IO ()

Either err a catches the error from the first time around and short circuits anyway at the first Left any way. So I personally like to structure my blocks in such a way that sequenced parts follow each other and any error is raised on execution and handled in one go where possible.

either in this scenario behaves like a standard case expression. maybe is a great function too.

I like keeping all of the returned Functors/Monads separate and building them up or breaking out of them to get the pure values as required. IO is the only one I don’t think ever makes sense break out of for no reason.

Sometimes I will stack them but only as the results of doing something like a traverse/mapM or Applicative application with <*>; and even then I’m stripping off whichever layer stops making sense for the name that it’s bound to as soon as possible.

IMO the blog post should be understood in the appropriate context. From the post:

One commonly stated position goes something like “all exceptions should be explicit at the type level<…>”

At that time it was another surge of this purism, and that’s what Snoyman was arguing against. Regarding ExceptT over IO… While he definitely meant it, I think it was an overreaction/overgeneralization to declare it an anti-pattern. In any case it was not the main point of his post IIRC.

In your particular case ExceptT AppError IO is almost certainly an anti-pattern though, because you never do exhaustive pattern matching on AppError. (In fact I didn’t find where you pattern match on it at all, but I didn’t dig too deep.) So it makes total sense to just throw an exception. But IMO it’s totally OK to have application-specific errors (assuming you actually do exhaustive pattern matching on it,) just don’t pretend they capture all possible ways IO code could fail.

You’re right actually, I’m never pattern matching on the AppError. All it does at the moment is to print them as they are and calling fail so it prints "user error: MyErrorType x y z".

This was useful for me for debugging on the command line, but probably not for users (if there will ever be any).

This reminds me I should handle those AppErrors. For the CLI I suppose I could just map them to a nice error message. Later on, when I eventually add a web interface (that was the original idea behind the app - to be able to handle those playlists in a browser), I’ll handle them somehow..

There’s also fromEither and fromEitherM in unliftio, if the Either contains the exception already. But yes, I like this style better than ExceptT

You can also use QualifiedDo, this was authored by my soon-to-be ex-colleague David:

{-# LANGUAGE QualifiedDo #-}
module Main where

 import ReturningEitherSyntax qualified

 doStuff :: IO ()
 doStuff = do
  result <- parseStuff
  either (throwIO . ParseException) pure result
  ...

 parseStuff :: IO (Either ParseError Parsed2)
 parseStuff =
  ReturningEitherSyntax.do
    f <- Right <$> readFile "..."
    parsed <- pure (parseFile f)
    f2 <- Right <$> readFile2 (getFilename parsed)
    pure (parseFile2 f2)

module ReturningEitherSyntax
  ( (>>=)
  , (>>)
  ) where

 import Control.Monad qualified as Monad
 import Prelude hiding ((>>), (>>=))

 (>>=) :: Monad m => m (Either e a) -> (a -> m (Either e b)) -> m (Either e b)
 first >>= next =
  first Monad.>>= either (pure . Left) next

 (>>) :: Monad m => m (Either e a) -> m (Either e b) -> m (Either e b)
 first >> next =
  first >>= const next

I don’t agree with Snoyman’s article. Some parts of it I agree with, but over all I don’t think it does a good job of charting a way forward. Let’s have a look through it.

A common (bad) design pattern I see is something like the following:

myFunction :: String -> ExceptT
MyException IO Int

There are (at least) three problems with this:

  1. It’s non-composable . If someone else has a separate exception type HisException, these two functions do not easily compose.

I don’t consider this a very strong argument. It’s straightforward to mediate between different exception types, even going all the way to SomeException or Dynamic if necessary!

  1. It gives an implication which is almost certainly false, namely: the only exception that can be thrown from this function is MyException. Almost any IO code in there will have the ability to throw some other type of exception, and additionally, almost any async exception can be thrown even if no synchronous exception is possible.

As far as the first part goes, I don’t think it gives the claimed implication, but to some extent implications are subjective so I’m not sure it’s possible to resolve that difference. However, the second part is not subjective: IO does have the ability to throw any other type of exception, and indeed any async exception may be raised in it. But that’s true of pure code too!

  1. You haven’t limited the possibility of exceptions, you’ve only added one extra avenue by which an exception can be thrown. myFunction can now either throwE or liftIO . throwIO.

This is just point 2 restated.

Mask-them-all anti-pattern

No disagreement with this part.

Instead, wouldn’t it be nice if the types of our functions were:

lookup :: Eq k => k -> [(k, v)] -> Either (KeyNotFound k) v

f :: SomeVal -> SomeVal -> SomeVal -> > Either F'sExceptionType F'sResult

The problem is that these types don’t unify.

Right, that is a problem, but very cheap to solve, and at least we’ve made invalid behaviours unrepresentable. The suggestion to use MonadThrow creates a problem that can’t be solved:

lookup :: (MonadThrow m, Eq k) => k -> [(k, v)] -> m v
f :: MonadThrow m => SomeVal -> SomeVal -> SomeVal -> m F'sResult

This is isomorphic to

lookup :: Eq k => k -> [(k, v)] -> Either SomeException v
f :: SomeVal -> SomeVal -> SomeVal -> Either SomeException F'sResult

and that has a lot of impossible behaviours that remain representable (all the other possible values of SomException that are not used). It’s very sad to use Haskell whilst tolerating such loose types!


So what’s my recommendation?

Well, for small projects it doesn’t matter. Choose anything and see how you like it. It won’t be too hard to change later. For large projects I don’t recommend MTL or transformers, for reasons of performance and composability (synthetic effects like transformers have poor performance everywhere they can’t be completely inlined, and they don’t support bracket which is essential in real world programs that have to manage external resources. Doing everything in IO is not terrible, I guess, but it does fall short on the “make invalid behaviours unreperesntable” metric, which is a sad outcome if one has already made the choice to use Haskell. (ReaderT _ IO/rio are common ways to echieve that.

But using an analytic effect system gives you the best of all worlds: you can represent all the precise exceptions in the type signature (accepting that we can do anything about about imprecise (thrown from pure code) or async exceptions anyway). We could even, though it hasn’t been done, write an I/O standard library that explicitly represents all exceptions that primitive I/O operations can perform (e.g. no ambient “file not found exception” from readFile, etc.).

I think a nice middle ground might be scoped exceptions:

try :: (Throw exn -> IO a) -> IO (Either exn a)
throw :: Throw exn -> exn -> IO a

similar to Bluefin’s Throw capability but without the Bluefin scope tracking. You get a lot of informal “make invalid behaviour unrepresentable” improvement without paying the cost of scope tracking. Perhaps I’ll publish something like that.

Thanks for sharing this follow-up thoughts - much appreciated although to be honest I’ve never used effect systems or any of the mentioned alternatives so I’m feeling a bit lost here :smiley:

By the way, “analytic effect system” has been mentioned a couple of times… what does it mean (and what is an effect system for that matter)? Is there some place where I can look this stuff up?(I’m currently revisiting G. Hutton’s “Programming in Haskell 2nd edition” after a few years’ break but this topics are definitely out of scope for that book).

The topic of effect systems is a very big one! You might like my talk: A History of Effect Systems. “Analytic” and “synthetic” are terms I introduced to distinguish two different ways of implementing effects:

  1. Synthetic: build data structures that represent computations and then interpret them. This is what transformers are, so for example exceptions are modelled by ExceptT e m r = m (Either e r), state is modelled by StateT s m r = s -> m (s, r) etc.. One problem with this approach is that it is slow. Alexis King explained why in Effects for Less. Another problem is that it’s hard to use them in a resource safe way, that is, you can’t implement a generic bracket.

  2. Analytic: take IO and use the type system to carve out pieces that behave in well-defined ways, such as use State Int :> es to indicate that an operation can mutate an Int state. These were invented either by Launchbury and Peyton Jones (ST) or Rybczak (effectful) depending on what you think counts as analytic[1].

This is also explained in the documentation of Bluefin (my effect system (analytic)), Why even use an effect system?.


But, if all you want to do is to get started with Haskell by writing some small useful programs then you don’t need to dive into this stuff. Only explore further if you’re genuinely curious, otherwise you’ll be distracted from your true goal :slight_smile:


[1]: I’m pretty sure effectful was the first fully-fledged analytic effect system, but I haven’t done an exhaustive search.

Transformers are not the right example. Alexis King’s talk is about when the monadic bind function is not statically known (or more precisely when it builds a tree of constructors or closures). Many transformers like your ExceptT and StateT examples do not do that. The problematic thing is using MTL-style polymorphism (e.g., MonadFoo m => m ()), or even fancier synthetic effect systems.

Since the discussion is ongoing I’d like to add my take. Unfortunately it will be a half reply/half brain dump since I can’t think of a better way to format it.

I think that generally a Haskell programmer has to eventually learn how all the major styles work to navigate the ecosystem. EDIT: Removed unhelpful sentence here that had a bad tone.

If a library exports a transformer (including mtl) interface then you have to use that, if it exports an effect system interface then you have to use that. You will need to write some glue code between the library interface and whatever you chose as your main style for your app (unless there’s some magic that does this?). rio is widely used so you will also run into it or see it discussed.

If you are a library developer and you want a lot of people to be able to pick up and use your library quickly the best external interface for code with side effects is plain IO even if you use an effect system internally. This would also apply to an internal company library that is shared between multiple Haskell apps that could have different styles.

For me personally I’m trying to find a Haskell style that I enjoy working with, that is easy to refactor, and that I could get other people up to speed on quickly. The issue I have with most of the main styles is that they steer you into having a type that is spread throughout your whole app. This also makes all the type signatures in the app more complicated. Effect systems make it modular but there’s still the base effect type that gets spread everywhere. I’m ok with IO everywhere because it’s part of the language.

If I have IO readily at hand then I can turn complicatedPureFunctionA $ complicatedPureFunctionB myData into

do
  resultB = complicatedPureFunctionB myData
  log resultB
  resultA = complicatedPureFunctionA resultB

without changing any type signatures or using unsafe functions. The majority of the code can stay pure with IO code acting as a “supervisor” or “scheduler” of pure computations.

My preference would be to only bring in types as needed so that most of the code has simple type signatures. In a simple web app a lot of the time the only resource required is a database connection and I can acquire one as needed rather than having a transaction per request. Keeping the types simple also makes it easier to refactor the code.

From a caller’s standpoint IO (Either) is clear and easy to read. The problems that come up are handling early exits without verbose code and multiple error types.

For the first problem ExceptT handles it and it’s a good example of how functionality can be added to a monad. It’s easy to learn and you can understand it as a nicer IO (Either) without knowing anything else about transformers other than lifting . The issues as you mentioned are the performance issues and resource handling. I haven’t dug into it much but as far as I could tell the performance issues seem to come up when you stack more than one transformer and possibly some types of loops. From the effectful docs it looks like resource handling breaks because of the way typeclasses are used to collapse lifts? If it can be fixed with either limiting the stack to ExceptT IO or adding some wrapping/unwrapping code I would be ok with using that to keep the readability benefits.

The second problem of multiple error types is where I would want something that looks like

ExceptT e1 IO a -> (e1 -> ExceptT e2 IO a) -> ExceptT e2 IO a

This is sort of like catchExceptT from @jaror but allows you to remap the error or map the error into a successful result. This would let you log some fine-grained error data and handle cases that you might not want to propagate up. For example an UnauthorizedError far down the call stack might have some data that allows it to be handled by the caller but should not be exposed externally. Picking where to split the types is tricky so I definitely don’t have that figured out yet.

A minor issue with ExceptT is the one mentioned in the article which is that it introduces a new kind of exception. Bringing in other languages is distracting but this is where I prefer the interface of Scala Cats EitherT . The names keep the concept separate from a notion of an exception and emphasizes that it is a better IO(Either). It’s an error value not an exception.

Lastly I don’t think that using ExceptT would exclude me from using effectful or bluefin. Once I call into the effect system I can use the error handling of that system and map the error when I get out. This also means that I could experiment with both systems in the same app.

If a complicated transformer stack can do some fancy backtracking then I might want to try it out as well but isolate it to a part of the code that needs it to avoid performance issues.

I didn’t dicuss rio much mostly because you have to thread the one type through everywhere. I don’t think that this is bad but it’s not my preference. I think that the discussion around introducing the rio pattern improved Haskell best practices in general.

Knowing about rio and transformers makes reading the bluefin and effectful docs easier. That’s why I think that starting with ExceptT is the right path to learning all the styles.

I wouldn’t recommend the style I’m working on. I’m describing it mostly to argue against having one style to rule them all. In the unlikely case that I get to do Haskell for work then I would adapt to whatever style is being used. At the same time with the tradeoffs that the current styles offer I would rather mix and match in my personal projects.