fx-0.4: library/Fx.hs
module Fx
(
-- * Fx
Fx,
-- ** Environment handling
provideAndUse,
handleEnv,
-- ** Concurrency
start,
wait,
concurrently,
-- ** IO execution
-- |
-- These functions leak abstraction in one way or the other,
-- requiring you to ensure that your code doesn't throw unexpected exceptions.
-- `try` are `catch` are your tools for that.
--
-- Besides these functions `Fx` also has an instance of `MonadIO`,
-- which provides the only non-leaky way of running IO, catching all possible exceptions.
runTotalIO,
runPartialIO,
runExceptionalIO,
runSTM,
-- * Provider
Provider,
acquireAndRelease,
pool,
-- * Future
Future,
-- * Conc
Conc,
-- * Classes
-- ** FxRunning
FxRunning(..),
-- ** ErrHandling
ErrHandling(..),
exposeErr,
absorbErr,
-- ** EnvMapping
EnvMapping(..),
-- * Exceptions
FxException(..),
FxExceptionReason(..),
)
where
import Fx.Prelude hiding (app)
import qualified Data.Text as Text
import qualified Data.HashSet as HashSet
import qualified Fx.Strings as Strings
-- * IO
-------------------------
{-|
Execute an effect with no environment and all errors handled.
Conventionally, this is what should be placed in the @main@ function.
-}
runFxInIO :: Fx () Void res -> IO res
runFxInIO (Fx m) = uninterruptibleMask $ \ unmask -> do
fatalErrChan <- newTQueueIO
resVar <- newEmptyTMVarIO
forkIO $ do
tid <- myThreadId
finalize <- let
crash tids reason = atomically (writeTQueue fatalErrChan (FxException (tid : tids) reason))
fxEnv = FxEnv unmask crash ()
in
catch
(do
resOrVoid <- runExceptT (runReaderT m fxEnv)
return $ case resOrVoid of
Right res -> atomically (putTMVar resVar res)
Left a ->
catch
(do
evaluate a
crash [] (BugFxExceptionReason "Unexpected void")
)
(crash [] . ErrorCallFxExceptionReason)
)
(\ exc -> return $ case fromException exc of
-- Catch calls to `error`.
Just errorCall -> crash [] (ErrorCallFxExceptionReason errorCall)
-- Catch anything else we could miss. Just in case.
_ -> crash [] (BugFxExceptionReason (Strings.unexpectedException exc))
)
-- Throw errors or post the result
finalize
-- Wait for fatal error or result
join $ catch
(
unmask $ atomically $ asum
[
do
fatalErr <- readTQueue fatalErrChan
return $ throwIO fatalErr
,
do
res <- readTMVar resVar
return $ return res
]
)
(\ (exc :: SomeException) ->
case fromException exc of
Just (exc :: AsyncException) -> throwIO exc
_ -> throwIO (FxException [] (BugFxExceptionReason (Strings.failedWaitingForFinalResult exc))))
-- * Fx
-------------------------
{-|
Effectful computation with explicit errors in the context of provided environment.
Calling `fail` causes the whole app to interrupt outputting a message to console.
`fail` is intended to be used in events which you expect never to happen,
and hence which should be considered bugs.
It is similar to calling `fail` on IO,
with a major difference of the error never getting lost in a concurrent environment.
Calling `fail` results in `ErrorCallFxExceptionReason` in the triggerred `FxException`.
Thus in effect it is the same as calling the `error` function.
-}
newtype Fx env err res = Fx (ReaderT (FxEnv env) (ExceptT err IO) res)
deriving instance Functor (Fx env err)
deriving instance Applicative (Fx env err)
deriving instance Selective (Fx env err)
deriving instance Monoid err => Alternative (Fx env err)
deriving instance Monad (Fx env err)
deriving instance Monoid err => MonadPlus (Fx env err)
instance MonadFail (Fx env err) where
fail msg = Fx $ ReaderT $ \ (FxEnv _ crash _) -> liftIO $ do
crash [] (ErrorCallFxExceptionReason (ErrorCall msg))
fail "Crashed"
instance MonadIO (Fx env SomeException) where
liftIO io = Fx (ReaderT (\ (FxEnv unmask _ _) -> ExceptT (try (unmask io))))
instance Bifunctor (Fx env) where
bimap lf rf = mapFx (mapReaderT (mapExceptT (fmap (bimap lf rf))))
{-|
Runtime and application environment.
-}
data FxEnv env = FxEnv (forall a. IO a -> IO a) ([ThreadId] -> FxExceptionReason -> IO ()) env
mapFx fn (Fx m) = Fx (fn m)
{-|
Turn a non-failing IO action into an effect.
__Warning:__
It is your responsibility to ensure that it does not throw exceptions!
-}
runTotalIO :: IO res -> Fx env err res
runTotalIO io = Fx $ ReaderT $ \ (FxEnv unmask crash _) -> lift $
catch (unmask io)
(\ (exc :: SomeException) -> do
crash [] (UncaughtExceptionFxExceptionReason exc)
fail "Unhandled exception in runTotalIO. Got propagated to top."
)
{-|
Run IO which produces either an error or result.
__Warning:__
It is your responsibility to ensure that it does not throw exceptions!
-}
runPartialIO :: IO (Either err res) -> Fx env err res
runPartialIO io = runTotalIO io >>= either throwErr return
{-|
Run IO which only throws a specific type of exception.
__Warning:__
It is your responsibility to ensure that it doesn't throw any other exceptions!
-}
runExceptionalIO :: Exception exc => IO res -> Fx env exc res
runExceptionalIO io =
Fx $ ReaderT $ \ (FxEnv unmask crash _) -> ExceptT $
catch (fmap Right (unmask io)) $ \ exc -> case fromException exc of
Just exc' -> return (Left exc')
Nothing -> do
crash [] (UncaughtExceptionFxExceptionReason exc)
fail "Unhandled exception in runExceptionalIO. Got propagated to top."
{-|
Run STM, crashing in case of STM exceptions.
Same as @`runTotalIO` . `atomically`@.
-}
runSTM :: STM res -> Fx env err res
runSTM = runTotalIO . atomically
{-|
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 `wait` 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 `wait`.
-}
start :: Fx env err res -> Fx env err' (Future err res)
start (Fx m) =
Fx $ ReaderT $ \ (FxEnv unmask crash env) -> lift $ do
futureVar <- newEmptyTMVarIO
forkIO $ do
tid <- myThreadId
let childCrash tids dls = crash (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 [] (ErrorCallFxExceptionReason errorCall)
-- Catch anything else we could miss. Just in case.
_ -> crash [] (BugFxExceptionReason (Strings.unexpectedException exc))
atomically (putTMVar futureVar (Left Nothing))
)
finalize
return $ Future $ Compose $ readTMVar futureVar
{-|
Block until the future completes either with a result or an error.
-}
wait :: Future err res -> Fx env err res
wait (Future m) = Fx $ ReaderT $ \ (FxEnv unmask crash env) -> 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 [] (BugFxExceptionReason (Strings.failedWaitingForResult exc))
fail "Thread crashed with uncaught exception waiting for result."
)
{-|
Execute concurrent effects.
-}
concurrently :: Conc env err res -> Fx env err res
concurrently (Conc fx) = fx
{-|
Execute Fx in the scope of a provided environment.
-}
provideAndUse :: Provider err env -> Fx env err res -> Fx env' err res
provideAndUse (Provider (Fx acquire)) (Fx fx) =
Fx $ ReaderT $ \ (FxEnv unmask crash _) -> ExceptT $ do
let providerFxEnv = FxEnv unmask crash ()
acquisition <- runExceptT (runReaderT acquire providerFxEnv)
case acquisition of
Left err -> return (Left err)
Right (env, (Fx release)) -> do
resOrErr <- runExceptT (runReaderT fx (FxEnv unmask crash env))
releasing <- runExceptT (runReaderT release providerFxEnv)
return (resOrErr <* releasing)
{-|
Collapse an env handler into an environmental effect.
__Warning:__
This function leaks the abstraction over the environment.
It is your responsibility to ensure that you don't use it to return
the environment and use it outside of the handler's scope.
-}
handleEnv :: (env -> Fx env err res) -> Fx env err res
handleEnv handler =
Fx $ ReaderT $ \ (FxEnv unmask crash env) ->
case handler env of
Fx rdr -> runReaderT rdr (FxEnv unmask crash env)
-- * Future
-------------------------
{-|
Handle to a result of an action which may still be being executed on another thread.
The way you deal with it is thru the `start` and `wait` functions.
-}
newtype Future err res = Future (Compose STM (Either (Maybe err)) res)
deriving (Functor, Applicative)
{-|
Decides whether to wait for the result of another future.
-}
deriving instance Selective (Future err)
instance Bifunctor Future where
bimap lf rf = mapFuture (mapCompose (fmap (bimap (fmap lf) rf)))
mapFuture fn (Future m) = Future (fn m)
-- * Conc
-------------------------
{-|
Wrapper over `Fx`,
whose instances compose by running computations on separate threads.
You can turn `Fx` into `Conc` using `runFx`.
-}
newtype Conc env err res = Conc (Fx env err res)
deriving instance Functor (Conc env err)
deriving instance Bifunctor (Conc env)
instance Applicative (Conc env err) where
pure = Conc . pure
(<*>) (Conc m1) (Conc m2) = Conc $ do
future1 <- start m1
res2 <- m2
res1 <- wait future1
return (res1 res2)
{-|
Spawns a computation,
deciding whether to wait for it.
-}
instance Selective (Conc env err) where
select (Conc choose) (Conc act) = Conc $ do
actFtr <- start act
chooseRes <- choose
case chooseRes of
Left a -> do
aToB <- wait actFtr
return (aToB a)
Right b -> return b
mapConc fn (Conc m) = Conc (fn m)
-- * Provider
-------------------------
{-|
Effectful computation with explicit errors,
which encompasses environment acquisition and releasing.
Composes well, allowing you to merge multiple providers into one.
Builds up on ideas expressed in http://www.haskellforall.com/2013/06/the-resource-applicative.html
and later released as the \"managed\" package.
-}
newtype Provider err env = Provider (Fx () err (env, Fx () err ()))
instance Functor (Provider err) where
fmap f (Provider m) = Provider $ do
(env, release) <- m
return (f env, release)
instance Applicative (Provider err) where
pure env = Provider (pure (env, pure ()))
Provider m1 <*> Provider m2 = Provider $
liftA2 (\ (env1, release1) (env2, release2) -> (env1 env2, release2 *> release1)) m1 m2
instance Monad (Provider err) where
return = pure
(>>=) (Provider m1) k2 = Provider $ do
(env1, release1) <- m1
(env2, release2) <- case k2 env1 of Provider m2 -> m2
return (env2, release2 >> release1)
instance MonadIO (Provider SomeException) where
liftIO = runFx . liftIO
instance Bifunctor Provider where
bimap lf rf (Provider m) = Provider (bimap lf (bimap rf (first lf)) m)
second = fmap
{-|
Create a resource provider from acquiring and releasing effects.
-}
acquireAndRelease :: Fx () err env -> (env -> Fx () err ()) -> Provider err env
acquireAndRelease acquire release = Provider $ do
env <- acquire
return (env, release env)
{-|
Convert a single resource provider into a pool provider.
The wrapper provider acquires the specified amount of resources using the original provider,
and returns a modified version of the original provider,
whose acquisition and releasing merely takes one resource out of the pool and puts it back when done.
No actual acquisition or releasing happens in the wrapped provider.
No errors get raised in it either.
Use this when you need to access a resource concurrently.
-}
pool :: Int -> Provider err env -> Provider err (Provider err' env)
pool poolSize (Provider acquire) = Provider $ do
queue <- runSTM newTQueue
replicateM_ poolSize $ do
handle <- acquire
runSTM $ writeTQueue queue handle
let
resourceProvider = Provider $ do
(env, releaseResource) <- runSTM $ readTQueue queue
return (env, runSTM (writeTQueue queue (env, releaseResource)))
release = do
releasers <- runSTM $ do
list <- flushTQueue queue
guard (length list == poolSize)
return (fmap snd list)
sequence_ releasers
in return (resourceProvider, release)
-- * Classes
-------------------------
-- ** Fx Running
-------------------------
{-|
Support for running of `Fx`.
Apart from other things this is your interface to turn `Fx` into `IO` or `Conc`.
-}
class FxRunning env err m | m -> env, m -> err where
runFx :: Fx env err res -> m res
{-|
Executes an effect with no environment and all errors handled.
-}
instance FxRunning () Void IO where
runFx = runFxInIO
instance FxRunning () err (ExceptT err IO) where
runFx fx = ExceptT (runFx (exposeErr fx))
instance FxRunning env err (ReaderT env (ExceptT err IO)) where
runFx fx = ReaderT (\ env -> ExceptT (runFx (mapEnv (const env) (exposeErr fx))))
{-|
Executes an effect with no environment and all errors handled in `Fx`
with any environment and error.
Same as @(`mapEnv` (`const` ()) . `first` `absurd`)@.
-}
instance FxRunning () Void (Fx env err) where
runFx = mapEnv (const ()) . first absurd
instance FxRunning env err (Conc env err) where
runFx = Conc
instance FxRunning () err (Provider err) where
runFx fx = Provider (fmap (\ env -> (env, pure ())) fx)
-- ** ErrHandling
-------------------------
{-|
Support for error handling.
Functions provided by this class are particularly helpful,
when you need to map into error of type `Void`.
-}
class ErrHandling m where
{-|
Interrupt the current computation raising an error.
-}
throwErr :: err -> m err res
{-|
Handle error in another failing action.
Sort of like a bind operation over the error type parameter.
-}
handleErr :: (a -> m b res) -> m a res -> m b res
{-|
Expose the error in result,
producing an action, which is compatible with any error type.
-}
exposeErr :: (ErrHandling m, Functor (m a), Applicative (m b)) => m a res -> m b (Either a res)
exposeErr = absorbErr Left . fmap Right
{-|
Map from error to result, leaving the error be anything.
-}
absorbErr :: (ErrHandling m, Applicative (m b)) => (a -> res) -> m a res -> m b res
absorbErr fn = handleErr (pure . fn)
instance ErrHandling (Fx env) where
throwErr = Fx . lift . throwE
handleErr handler = mapFx $ \ m -> ReaderT $ \ unmask -> ExceptT $ do
a <- runExceptT (runReaderT m unmask)
case a of
Right res -> return (Right res)
Left err -> case handler err of
Fx m -> runExceptT (runReaderT m unmask)
instance ErrHandling Future where
throwErr = Future . Compose . return . Left . Just
handleErr handler = mapFuture $ \ m -> Compose $ do
a <- getCompose m
case a of
Right res -> return (Right res)
Left b -> case b of
Just err -> case handler err of
Future m' -> getCompose m'
Nothing -> return (Left Nothing)
deriving instance ErrHandling (Conc env)
-- ** Env Mapping
-------------------------
{-|
Support for mapping of the environment.
-}
class EnvMapping m where
{-|
Map the environment.
Please notice that the expected function is contravariant.
-}
mapEnv :: (b -> a) -> m a err res -> m b err res
instance EnvMapping Fx where
mapEnv fn (Fx m) =
Fx $ ReaderT $ \ (FxEnv unmask crash env) ->
runReaderT m (FxEnv unmask crash (fn env))
deriving instance EnvMapping Conc
-- * Exceptions
-------------------------
{-|
Fatal failure of an `Fx` application.
Informs of an unrecoverable condition that the application has reached.
It is not meant to be caught,
because it implies that there is either a bug in your code or
a bug in the "fx" library itself, which needs reporting.
Consists of a list of thread identifiers specifying the nesting path of
the faulty thread and the reason of failure.
-}
data FxException = FxException [ThreadId] FxExceptionReason
instance Show FxException where
show (FxException tids reason) = Strings.fatalErrorAtThreadPath tids (show reason)
instance Exception FxException
{-|
Reason of a fatal failure of an `Fx` application.
-}
data FxExceptionReason =
UncaughtExceptionFxExceptionReason SomeException |
ErrorCallFxExceptionReason ErrorCall |
BugFxExceptionReason String
instance Show FxExceptionReason where
show = \ case
UncaughtExceptionFxExceptionReason exc -> Strings.uncaughtException exc
ErrorCallFxExceptionReason errorCall -> show errorCall
BugFxExceptionReason details -> Strings.bug details