@hackage oath0.1.1

Composable concurrent computation done right

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 Oaths 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 (<*>).

Performance

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