oath: Composable concurrent computation done right

[ bsd3, concurrency, library ] [ Propose Tags ]

See README.md for details


[Skip to Readme]

Modules

[Index] [Quick Jump]

Downloads

Maintainer's Corner

Package maintainers

For package maintainers and hackage trustees

Candidates

Versions [RSS] 0.0, 0.1.1
Change log CHANGELOG.md
Dependencies base (>=4.14.1.0 && <4.18), stm, stm-delay [details]
License BSD-3-Clause
Copyright Copyright (c) 2022 Fumiaki Kinoshita
Author Fumiaki Kinoshita
Maintainer fumiexcel@gmail.com
Category Concurrency
Bug tracker https://github.com/fumieval/oath
Source repo head: git clone https://github.com/fumieval/oath.git
Uploaded by FumiakiKinoshita at 2023-01-03T14:45:25Z
Distributions
Downloads 140 total (6 in the last 30 days)
Rating (no votes yet) [estimated by Bayesian average]
Your Rating
  • λ
  • λ
  • λ
Status Docs uploaded by user
Build status unknown [no reports yet]

Readme for oath-0.1.1

[back to package description]

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