Consider the following

notifyOutdatedHaskellDependencies :: MonadEffects '[Email, Hackage] m => m ()
notifyOutdatedHaskellDependencies =
  findPackagesWithOutdatedDependencies >>= traverse_ notifyAuthor

This is a perfectly reasonable program to write - we find all Hackage packages with outdated dependencies which provides us with some kind of list, and then we traverse_ over this list to notify the author of each package. As it stands, this is an optimistic view on the world - apparently nothing can go wrong!

Interpreters Can Fail

The world is not so kind, there are many ways this can and will go wrong at runtime. For example, if we're going to check hackage.haskell.org, we can hardly expect this to work if we don't have a network connection:

hackageHttp :: (MonadThrow m, MonadIO m) => Implementation Hackage m a -> m a

This isn't so bad - if we can't get packages then we don't about emails not getting sent. But sending emails could also fail:

emailSMTP :: (MonadThrow m, MonadIO m) => Implementation Email m a -> m a

Suppose we find 5 outdated packages. We email the first person, but then the second email fails to send due to some transient network error. If we tried again that email might send, but in actuality the whole thing will explode and we'll terminate.

So use exception effects?

We could use some kind of effect that lets us catch exceptions in our high-level program:

notifyOutdatedHaskellDependencies
  :: MonadEffects '[Email, Hackage, Exceptions] m => m ()
notifyOutdatedHaskellDependencies =
      findPackagesWithOutdatedDependencies
  >>= traverse_ (try notifyAuthor)

But why should we have to? Does this program care about failure? Afterall, there are perfectly reasonable interpretations of this that don't fail - but now it suggests they do! Furthermore, how do we deal with the case that sending an email might work on the 2nd try? Just run the notification again?

notifyOutdatedHaskellDependencies
  :: MonadEffects '[Email, Hackage, Exceptions] m => m ()
notifyOutdatedHaskellDependencies =
      findPackagesWithOutdatedDependencies
  >>= traverse_
        (     try notifyAuthor 
          >>= either (\\SomeException{} -> notifyAuthor) return
        )

That's not very satisfying, how do we know that in general immediately retrying is suitable? What if we implement the Email effect using something like Mandrill and encounter an API rate limit? In this case the better strategy is actually to sleep.

Maybe even higher level effects are the answer?

Maybe the effect system itself is the way to think about this - https://www.notion.so/ocharles/Exceptions-from-interpreters-f26dbf6c512844ec91634a448832036c#f3f4090b44994616856bbf988e694120 kind of suggests this.

An alternative formulation could be

notifyOutdatedHaskellDependencies :: MonadEffect NotifyDependencies m => m ()
notifyOutdatedHaskellDependencies =
  findPackagesWithOutdatedDependencies >>= traverse_ notifyAuthor

Now we just have to provide implementations of findPackagesWithOutdatedDependencies and can choose the retrying strategy when we do so.