{-# LANGUAGE DataKinds #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE PolyKinds #-} {-# LANGUAGE RankNTypes #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TupleSections #-} {-# LANGUAGE TypeOperators #-} {-# LANGUAGE UndecidableInstances #-} module Control.Monad.Trans.Fraxl ( -- * The Fraxl Monad FreerT , Fraxl , Fetch , runFraxl , simpleAsyncFetch , fetchNil , (|:|) , hoistFetch , transFetch -- * The Sequence of Effects , ASeq(..) , reduceASeq , hoistASeq , traverseASeq , rebaseASeq -- * Caching , CachedFetch(..) , fetchCached , runCachedFraxl , evalCachedFraxl , module Data.GADT.Compare -- * Union , Union(..) , unconsCoRec ) where import Control.Applicative.Free.Fast import Control.Arrow import Control.Concurrent import Control.Concurrent.Async import Control.Monad import Control.Monad.IO.Class import Control.Monad.State import Control.Monad.Trans.Fraxl.Free import Data.Dependent.Map (DMap) import qualified Data.Dependent.Map as DMap import Data.GADT.Compare import Data.Sum -- | Fraxl is based on a particular Freer monad. -- This Freer monad has applicative optimization, -- which is used to parallelize effects. type FreerT f = FreeT (Ap f) -- | Fraxl is just the 'FreerT' monad transformer, applied with 'Union'. -- This is because 'Fraxl' is just a free monad over a variety of data sources. type Fraxl r = FreerT (Union r) -- | A data source is an effect @f@ that operates in some monad @m@. -- Given a sequence of effects, -- a data source should use @m@ to prepare a corresponding sequence of results. type Fetch f m a = ASeq f a -> m (ASeq m a) -- | Fetch empty union. -- Only necessary to terminate a list of 'Fetch' functions for @Fetch (Union r)@ fetchNil :: Applicative m => Fetch (Union '[]) m a fetchNil ANil = pure ANil fetchNil _ = error "Not possible - empty union" -- | Like '(:)' for constructing @Fetch (Union (f ': r))@ (|:|) :: forall f r a m. (Monad m) => (forall a'. Fetch f m a') -> (forall a'. Fetch (Union r) m a') -> Fetch (Union (f ': r)) m a (fetch |:| fetchU) list = (\(_, _, x) -> x) <$> runUnion ANil ANil list where runUnion :: ASeq f x -> ASeq (Union r) y -> ASeq (Union (f ': r)) z -> m (ASeq m x, ASeq m y, ASeq m z) runUnion flist ulist ANil = (, , ANil) <$> fetch flist <*> fetchU ulist runUnion flist ulist (ACons (Union u) us) = case unconsCoRec u of Left fa -> fmap (\(ACons ma ms, other, rest) -> (ms, other, ACons ma rest)) (runUnion (ACons fa flist) ulist us) Right u' -> fmap (\(other, ACons ma ms, rest) -> (other, ms, ACons ma rest)) (runUnion flist (ACons (Union u') ulist) us) infixr 5 |:| -- | Hoist a 'Fetch' function into a different result monad. hoistFetch :: Functor m => (forall x. m x -> n x) -> Fetch f m a -> Fetch f n a hoistFetch u f = u . fmap (hoistASeq u) . f -- | Translate a 'Fetch' function from @f@ requests, to @g@ requests. transFetch :: (forall x. g x -> f x) -> Fetch f m a -> Fetch g m a transFetch u f list = f (hoistASeq u list) -- | Runs a Fraxl computation, using a given 'Fetch' function for @f@. -- This takes 'FreerT' as a parameter rather than 'Fraxl', -- because 'Fraxl' is meant for a union of effects, -- but it should be possible to run a singleton effect. runFraxl :: Monad m => (forall a'. Fetch f m a') -> FreerT f m a -> m a runFraxl fetch = iterT $ \a -> unAp a (\f s -> join (reduceASeq <$> fetch s) >>= f) (const id) ANil -- | A simple method of turning an 'IO' bound computation -- into a concurrent 'Fetch'. simpleAsyncFetch :: MonadIO m => (forall x. f x -> IO x) -> Fetch f m a simpleAsyncFetch fetchIO = traverseASeq (fmap (liftIO . wait) . liftIO . async . fetchIO) -- | Caching in Fraxl works by translating @FreerT f@ into -- @FreerT (CachedFetch f)@, then running with 'CachedFetch''s DataSource. -- That instance requires 'f' to to have a 'GCompare' instance. -- -- The 'CachedFetch' instance uses a 'MonadState' to track cached requests. -- The state variable is a 'DMap' from the 'dependent-map' package. -- Keys are requests, and values are 'MVar's of the results. newtype CachedFetch f a = CachedFetch (f a) fetchCached :: forall t m f a. ( Monad m , MonadTrans t , MonadState (DMap f MVar) (t m) , GCompare f , MonadIO (t m)) => (forall a'. Fetch f m a') -> Fetch (CachedFetch f) (t m) a fetchCached fetch list = snd <$> runCached ANil list where runCached :: ASeq f x -> ASeq (CachedFetch f) y -> t m (ASeq (t m) x, ASeq (t m) y) runCached flist ANil = (, ANil) <$> lift (hoistASeq lift <$> fetch flist) runCached flist (ACons (CachedFetch f) fs) = do cache <- get case DMap.lookup f cache of Just mvar -> fmap (second (ACons (liftIO $ readMVar mvar))) (runCached flist fs) Nothing -> do (mvar :: MVar z) <- liftIO newEmptyMVar put (DMap.insert f mvar cache) let store :: t m z -> t m z store m = m >>= \a -> liftIO (putMVar mvar a) >> return a fmap (\(ACons m ms, rest) -> (ms, ACons (store m) rest)) (runCached (ACons f flist) fs) -- | Runs a Fraxl computation with caching using a given starting cache. -- Alongside the result, it returns the final cache. runCachedFraxl :: forall m f a. ( MonadIO m , GCompare f) => (forall a'. Fetch f m a') -> FreerT f m a -> DMap f MVar -> m (a, DMap f MVar) runCachedFraxl fetch a cache = let cachedA :: FreerT (CachedFetch f) (StateT (DMap f MVar) m) a cachedA = transFreeT (hoistAp CachedFetch) (hoistFreeT lift a) in runStateT (runFraxl (fetchCached fetch) cachedA) cache -- | Like 'runCachedFraxl', except it starts with an empty cache -- and discards the final cache. evalCachedFraxl :: forall m f a. ( MonadIO m , GCompare f) => (forall a'. Fetch f m a') -> FreerT f m a -> m a evalCachedFraxl fetch a = fst <$> runCachedFraxl fetch a DMap.empty unconsCoRec :: Sum (t ': ts) f -> Either (t f) (Sum ts f) unconsCoRec s = case decompose s of Left s' -> Right s' Right found -> Left found -- | @Union@ represents a value of any type constructor in @r@ applied with @a@. newtype Union ts f = Union (Sum ts f) instance GEq (Union '[]) where _ `geq` _ = error "Not possible - empty union" instance (GEq f, GEq (Union r)) => GEq (Union (f ': r)) where Union a `geq` Union b = case (unconsCoRec a, unconsCoRec b) of (Left fa, Left fb) -> fa `geq` fb (Right a', Right b') -> Union a' `geq` Union b' _ -> Nothing instance GCompare (Union '[]) where _ `gcompare` _ = error "Not possible - empty union" instance (GCompare f, GCompare (Union r)) => GCompare (Union (f ': r)) where Union a `gcompare` Union b = case (unconsCoRec a, unconsCoRec b) of (Left fa, Left fb) -> fa `gcompare` fb (Right a', Right b') -> Union a' `gcompare` Union b' (Left _, Right _) -> GLT (Right _, Left _) -> GGT