Oath: composable concurrent computation done right
Oath is an Applicative structures that makes concurrent actions composable.
newtype Oath a = Oath { runOath :: forall r. (STM a -> IO r) -> IO r }
Oath
is a continuation-passing IO action which takes a transaction to obtain the final result (STM a
).
The continuation-passing style makes it easier to release resources in time.
The easiest way to construct Oath
is oath
. It runs the supplied IO action in a separate thread as long as the continuation is running.
oath :: IO a -> Oath a
oath act = Oath $ \cont -> do
v <- newEmptyTMVarIO
tid <- forkFinally act (atomically . putTMVar v)
let await = takeTMVar v >>= either throwSTM pure
cont await `finally` killThread tid
evalOath :: Oath a -> IO a
evalOath m = runOath m atomically
Oath
is an Applicative
, so you can combine multiple Oath
s using <*>
. Oath
combined this way kicks off computations without waiting for the results. The following code runs foo :: IO a
and bar :: IO b
concurrently, then applies f
to these results.
main = evalOath $ f <$> oath foo <*> oath bar
It does not provide a Monad instance because it is logically impossible to define one consistent with the Applicative instance.
Usage
Oath
abstracts a triple of sending a request, waiting for response, and cancelling a request. If you want to send requests in a deterministic order, you can construct Oath
directly instead of calling oath
.
Oath $ \cont -> bracket sendRequest cancelRequest (cont . waitForResponse)
Timeout behaviour can be easily added using the Alternative
instance and delay :: Int -> Oath ()
. a <|> b
runs both computations until one of them returns a result, then cancels the other.
-- | An 'Oath' that finishes once the given number of microseconds elapses
delay :: Int -> Oath ()
oath action <|> delay 100000
Comparison to other packages
future, caf and async seem solve the same problem. They define abstractions to asynchronous computations. async
has an applicative Concurrently
wrapper.
spawn does not define any datatype. Instead it provides an utility function for IO
(spawn :: IO a -> IO (IO a)
). It does not offer a way to cancel a computation.
promises provides a monadic interface for pure demand-driven computation. It has nothing to do with concurrency.
unsafe-promises creates an IO action that waits for the result on-demand using unsafeInterleaveIO
.
futures provides a wrapper of forkIO
. There is no way to terminate an action and it does not propagate exceptions.
promise has illegal Applicative and Monad instances; (<*>)
is not associative and its ap
is not consistent with (<*>)
.
bench "oath 10" $ nfIO $ O.evalOath $ traverse (O.oath . pure) [0 :: Int ..9]
bench "async 10" $ nfIO $ A.runConcurrently $ traverse (A.Concurrently . pure) [0 :: Int ..9]
Oath
's overhead of (<*>)
is less than Concurrently
. Unlike Concurrently
, <*>
itself does not fork threads.
All
oath 10: OK (1.63s)
5.78 μs ± 265 ns
async 10: OK (0.21s)
12.3 μs ± 767 ns
oath 100: OK (0.22s)
52.6 μs ± 4.4 μs
async 100: OK (0.23s)
109 μs ± 8.4 μs