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!
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.
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 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.