packages feed

oath (empty) → 0.0

raw patch · 6 files changed

+279/−0 lines, 6 filesdep +asyncdep +basedep +futures

Dependencies added: async, base, futures, oath, promise, stm, stm-delay, streamly, tasty-bench, unsafe-promises

Files

+ CHANGELOG.md view
@@ -0,0 +1,5 @@+# Revision history for oath++## 0.1.0.0 -- YYYY-mm-dd++* First version. Released on an unsuspecting world.
+ README.md view
@@ -0,0 +1,87 @@+Oath: composable concurrent computation done right+----++Oath is an Applicative structures that makes concurrent actions composable.++```haskell+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 run the supplied action in a separate thread as long as the continuation is running.++```haskell+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. It starts computations without waiting for the results. Run `evalOath` to get the final result. The following code run concurrently `foo :: IO a` and `bar :: IO b`, then applies `f` to these results.++```haskell+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`.++```haskell+Oath $ \cont -> bracket sendRequest cancelRequest (cont . waitForResponse)+```++Timeout behaviour can be easily added using the `Alternative` instance and `delay :: Int -> Oath ()`. Or more in general, `<|>` runs both computations until one of them finishes.++```haskell+-- | An 'Oath' that finishes once the given number of microseconds elapses+delay :: Int -> Oath ()++oath action <|> delay 100000+```++Comparison to other packages+----++[future](https://hackage.haskell.org/package/future-2.0.0/docs/Control-Concurrent-Future.html), [caf](https://hackage.haskell.org/package/caf-0.0.3/docs/Control-Concurrent-Futures.html) and [async](https://hackage.haskell.org/package/async-2.2.4/docs/Control-Concurrent-Async.html) seem solve the same problem. They define abstractions to asynchronous computations. `async` has an applicative `Concurrently` wrapper.++[spawn](https://hackage.haskell.org/package/spawn-0.3/docs/Control-Concurrent-Spawn.html) 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](https://hackage.haskell.org/package/promises-0.3/docs/Data-Promise.html) provides a monadic interface for pure demand-driven computation. It has nothing to do with concurrency.++[unsafe-promises](https://hackage.haskell.org/package/unsafe-promises-0.0.1.3/docs/Control-Concurrent-Promise-Unsafe.html) creates an IO action that waits for the result on-demand using `unsafeInterleaveIO`.++[futures](https://hackage.haskell.org/package/futures-0.1/docs/Futures.html) provides a wrapper of `forkIO`. There is no way to terminate an action and it does not propagate exceptions.++[promise](https://hackage.haskell.org/package/promise-0.1.0.0/docs/Control-Concurrent-Promise.html) has illegal Applicative and Monad instances; `(<*>)` is not associative and has a bind that's not consistent with `(<*>).`++Performance+----++```haskell+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+```
+ benchmarks/bench.hs view
@@ -0,0 +1,15 @@+{-# LANGUAGE RankNTypes #-}+import qualified Oath as O+import qualified Control.Concurrent.Async as A+import Test.Tasty.Bench+import qualified Streamly.Prelude as S++main :: IO ()+main = defaultMain+  [ 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]+  , bench "streamly 10" $ nfIO $ S.drain $ S.fromZipAsync $ traverse (S.fromEffect . pure) [0 :: Int ..9]+  , bench "oath 100" $ nfIO $ O.evalOath $ traverse (O.oath . pure) [0 :: Int ..99]+  , bench "async 100" $ nfIO $ A.runConcurrently $ traverse (A.Concurrently . pure) [0 :: Int ..99]+  , bench "streamly 100" $ nfIO $ S.toList $ S.fromZipAsync $ traverse (S.fromEffect . pure) [0 :: Int ..99]+  ]
+ oath.cabal view
@@ -0,0 +1,60 @@+cabal-version:      2.4+name:               oath+version:            0.0++-- A short (one-line) description of the package.+synopsis:           Composable concurrent computation done right++-- A longer description of the package.+description:        See README.md for details++-- A URL where users can report bugs.+bug-reports:        https://github.com/fumieval/oath++-- The license under which the package is released.+license:            BSD-3-Clause+author:             Fumiaki Kinoshita+maintainer:         fumiexcel@gmail.com++-- A copyright notice.+copyright:          Copyright (c) 2021 Fumiaki Kinoshita+category:           Concurrency+extra-source-files: CHANGELOG.md, README.md++source-repository head+  type: git+  location: https://github.com/fumieval/oath.git++library+    exposed-modules: Oath+    -- Modules included in this executable, other than Main.+    -- other-modules:++    -- LANGUAGE extensions used by modules in this package.+    -- other-extensions:+    build-depends:    base >=4.14.1.0 && <4.17, stm, stm-delay+    hs-source-dirs:   src+    default-language: Haskell2010+    ghc-options: -Wall -Wcompat++test-suite test+    build-depends: base >=4.14.1.0+        , futures+        , unsafe-promises+        , promise+        , oath+        , async+        , streamly+    type: exitcode-stdio-1.0+    hs-source-dirs: tests+    default-language: Haskell2010+    main-is: test.hs+    ghc-options: -Wall -Wcompat++benchmark bench+    type:                exitcode-stdio-1.0+    main-is:             bench.hs+    hs-source-dirs:      benchmarks+    build-depends:       base, tasty-bench, oath, async, streamly+    ghc-options:         -Wall -threaded -O2+    default-language:    Haskell2010
+ src/Oath.hs view
@@ -0,0 +1,54 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE DerivingVia #-}+module Oath+  ( Oath(..)+  , hoistOath+  , evalOath+  , oath+  , delay+  , timeout) where++import Control.Applicative+import Control.Concurrent+import Control.Concurrent.STM+import Control.Concurrent.STM.Delay+import Control.Exception+import Data.Monoid++-- 'Oath' is an 'Applicative' structure that collects results of one or more computations.+newtype Oath a = Oath { runOath :: forall r. (STM a -> IO r) -> IO r }+  deriving Functor+  deriving (Semigroup, Monoid) via Ap Oath a++hoistOath :: (forall x. STM x -> STM x) -> Oath a -> Oath a+hoistOath t (Oath m) = Oath $ \cont -> m $ cont . t++evalOath :: Oath a -> IO a+evalOath m = runOath m atomically++instance Applicative Oath where+  pure a = Oath $ \cont -> cont (pure a)+  Oath m <*> Oath n = Oath $ \cont -> m $ \f -> n $ \x -> cont (f <*> x)++instance Alternative Oath where+  empty = Oath $ \cont -> cont empty+  Oath m <|> Oath n = Oath $ \cont -> m $ \a -> n $ \b -> cont (a <|> b)++-- | Lift an IO action into an 'Oath', forking a thread.+-- When the continuation terminates, it kills the thread.+-- Exception thrown in the thread will be propagated to the result.+oath :: IO a -> Oath a+oath act = Oath $ \cont -> do+  v <- newEmptyTMVarIO+  tid <- forkFinally act (atomically . putTMVar v)+  let await = readTMVar v >>= either throwSTM pure+  cont await `finally` killThread tid++-- | An 'Oath' that finishes once the given number of microseconds elapses+delay :: Int -> Oath ()+delay dur = Oath $ \cont -> bracket (newDelay dur) cancelDelay (cont . waitDelay)++-- | Wrap an 'Oath'+timeout :: Int -> Oath a -> Oath (Maybe a)+timeout dur m = Just <$> m <|> Nothing <$ delay dur
+ tests/test.hs view
@@ -0,0 +1,58 @@+import qualified Oath+import qualified Futures+import qualified Control.Concurrent.Promise.Unsafe as UP+import qualified Control.Concurrent.Promise as Promise+import qualified Control.Concurrent.Async as Async+import Control.Applicative+import Control.Concurrent+import Control.Exception+import Data.Functor.Compose+import qualified Streamly.Prelude as S++action :: Int -> String -> Double -> IO ()+action order name dur = do+  -- delay a bit so that "Begin" gets printed in a consistent order+  threadDelay $ 1000 * 10 * order+  putStrLn $ "    Begin " <> name+  threadDelay (floor $ 1000000 * dur)+  putStrLn $ "    End " <> name+  `onException` putStrLn ("    Killed " <> name)++tester :: Applicative m => (m () -> IO ()) -> (IO () -> m ()) -> IO ()+tester run lift = do+  putStrLn "  Left:"+  run $ (lift (action 0 "foo" 0.1) *> lift (action 1 "bar" 0.3)) *> lift (action 2 "baz" 0.2)+  putStrLn "  Right:"+  run $ lift (action 0 "foo" 0.1) *> (lift (action 1 "bar" 0.3) *> lift (action 2 "baz" 0.2))++  putStrLn "  Left Error:"+  try (run $ lift (fail "Fail") *> lift (action 0 "success" 0.1)) >>= print'+  putStrLn "  Right Error:"+  try (run $ lift (action 0 "success" 0.1) *> lift (fail "Fail")) >>= print'+  where+    print' :: Either SomeException () -> IO ()+    print' = putStrLn . ("    "++) . show++main :: IO ()+main = do+  putStrLn "oath timeout:"+  Oath.evalOath $ Oath.oath (action 0 "delay" 1.0) <|> Oath.delay 200000+  threadDelay 10000++  putStrLn "oath: "+  tester Oath.evalOath Oath.oath+  putStrLn "async: "+  tester Async.runConcurrently Async.Concurrently+  putStrLn "futures:"+  tester ((>>=Futures.block) . getCompose) (Compose . Futures.fork)++  putStrLn "streamly:"+  tester (S.drain . S.fromZipAsync . S.fromAhead) S.fromEffect++  putStrLn "promise:"+  tester Promise.runPromise Promise.liftIO++  -- random+  putStrLn "unsafe-promises:"+  try (tester id UP.promise) >>= (print :: Either SomeException () -> IO ())+