packages feed

fx-0.11: library/Fx/Future.hs

-- |
-- Future abstraction for concurrent computations
module Fx.Future
  ( Future,
    async,
    await,
    cancel,
  )
where

import Fx.Fx
import Fx.Prelude
import qualified Fx.Strings as Strings
import GHC.Stack (callStack)

-- |
-- Handle to a result of an action which may still be being executed on another thread.
data Future err res
  = Future
      (Maybe ThreadId)
      (Compose STM (Either (Maybe err)) res)

instance Functor (Future err) where
  fmap f (Future tid result) = Future tid (fmap f result)

instance Applicative (Future err) where
  pure a = Future Nothing (pure a)
  Future tid1 resultF <*> Future tid2 resultA =
    Future (tid1 <|> tid2) (resultF <*> resultA)

instance Alternative (Future err) where
  empty = Future Nothing empty
  Future tid1 result1 <|> Future tid2 result2 =
    -- When one completes, cancel the other
    Future (tid1 <|> tid2) (result1 <|> result2)

instance Bifunctor Future where
  bimap lf rf = mapImpl (mapCompose (fmap (bimap (fmap lf) rf)))

mapImpl ::
  ( Compose STM (Either (Maybe err1)) res1 ->
    Compose STM (Either (Maybe err2)) res2
  ) ->
  Future err1 res1 ->
  Future err2 res2
mapImpl fn (Future tid m) = Future tid (fn m)

-- |
-- Spawn a thread and start running an effect on it,
-- returning the associated future.
--
-- Fatal errors on the spawned thread are guaranteed to get propagated to the top.
-- By fatal errors we mean calls to `error`, `fail` and uncaught exceptions.
--
-- Normal errors (the explicit @err@ parameter) will only propagate
-- if you use `await` at some point.
--
-- __Warning:__
-- It is your responsibility to ensure that the whole future executes
-- before the running `Fx` finishes.
-- Otherwise you will lose the environment in scope of which the future executes.
-- To achieve that use `await`.
async :: Fx env err res -> Fx env err' (Future err res)
async (Fx m) =
  Fx $ ReaderT $ \(FxEnv unmask crash env) -> lift $ do
    futureVar <- newEmptyTMVarIO

    let handler = \case
          ThreadKilled -> void $ atomically (tryPutTMVar futureVar (Left Nothing))
          exc -> throwIO exc
    tid <- forkIO $ handle handler do
      tid <- myThreadId

      let childCrash stack tids dls = crash stack (tid : tids) dls

      finalize <-
        catch
          ( do
              res <- runExceptT (runReaderT m (FxEnv unmask childCrash env))
              return (atomically (putTMVar futureVar (first Just res)))
          )
          ( \exc -> return $ do
              case fromException exc of
                -- Catch calls to `error`.
                Just errorCall -> crash callStack [] (ErrorCallFxExceptionReason errorCall)
                -- Catch anything else we could miss. Just in case.
                _ -> crash callStack [] (BugFxExceptionReason (Strings.unexpectedException exc))
              atomically (putTMVar futureVar (Left Nothing))
          )

      finalize

    return $ Future (Just tid) $ Compose $ readTMVar futureVar

-- |
-- Block until the future completes either with a result or an error.
await :: Future err res -> Fx env err res
await (Future _ m) =
  Fx $ ReaderT $ \(FxEnv unmask crash _) ->
    ExceptT $
      join $
        catch
          ( do
              futureStatus <- unmask (atomically (getCompose m))
              return $ case futureStatus of
                Right res -> return (Right res)
                Left (Just err) -> return (Left err)
                Left Nothing -> fail "Waiting for a future that crashed"
          )
          ( \(exc :: SomeException) -> return $ do
              crash callStack [] (BugFxExceptionReason (Strings.failedWaitingForResult exc))
              fail "Thread crashed with uncaught exception waiting for result."
          )

cancel :: Future err res -> Fx env err' ()
cancel (Future Nothing _) = return ()
cancel (Future (Just tid) _) = runTotalIO (\_ -> killThread tid `catch` \(_ :: SomeException) -> return ())