Unify `fork` and `async`
mitchellwrosen opened this issue · comments
Current API
data Thread a
fork :: Scope -> IO a -> IO (Thread a)
async :: Scope -> IO a -> IO (Thread (Either SomeException a))
await :: Thread a -> IO a
Idea 1: allow user to pick e
in async
data Thread a
fork :: Scope -> IO a -> IO (Thread a)
async :: Exception e => Scope -> IO a -> IO (Thread (Either e a))
await :: Thread a -> IO a
This would allow users to pick some root exception, not necessarily SomeException
, that is "checked" - and all other higher exceptions (including all async exceptions, always) are propagated to the parent, like fork
.
One downside here is it moves async
in the direction of fork
, and in fact async @Void
would be basically equivalent to fork
, so it might be a bit difficult to separate the two functions in one's mind.
Idea 2: move the e
into Thread
data Thread e a
fork :: Scope -> IO a -> IO (Thread Void a)
async :: Exception e => Scope -> IO a -> IO (Thread e a)
await :: Thread e a -> IO (Either e a)
This would move the concept of what exception a thread might be expected to throw into its type, which is kind of nice. For example, a Thread IOException Int
is a thread that may succeed with an Int
, or fail with an IOException
, but any other exception is truly unexpected, not evident in its type, and would be propagated to its parent.
One downside here is callers of await
on a Thread Void
have to deal with an absurd Left Void
case explicitly. This can be worked around with a type family, which adds complexity to the API, and requires users to apply a function in their heads when reading documentation:
type family Result e a where
Result Void a = a
Result e a = Either e a
await :: Thread e a -> IO (Result e a)
Idea 3: fully unify fork
and async
data Thread e a
fork :: SyncException e => Scope -> IO a -> IO (Thread e a)
await :: Thread e a -> IO (Result e a)
class Exception e => SyncException e where
type Result e a :: Type
notExported :: Either e a -> Result e a
instance {-# OVERLAPS #-} SyncException Void where
type Result Void a = a
notExported = either absurd id
instance Exception e => SyncException e where
type Result e a = Either e a
notExported = id
This is like idea 2, but gets rid of the fork
/async
distinction entirely. Unfortunately, this means we need a type class to allow us to return just an a
, not a Either Void a
, in the Thread Void
case, which was previously accomplished by having two separate functions.
I've named the type class SyncException
here, just as a reminder that in ki
, asynchronous exceptions are always propagated, but it could really be called anything, including (confusingly) Exception
.
Feedback from @seagreen: Idea 1 is the winner so far.
We think the type family + type class is a little much just to unify fork
and async
.
Plus, we had the nice realization that async
, when generalized such, looks a lot like try
. Maybe it should be renamed to forktry
?
forktry :: Exception e => Scope -> IO a -> IO (Thread (Either e a))
Done in d362680