achille-0.0.0: src/Achille/Internal.hs
{-# LANGUAGE DeriveFunctor #-}
-- | Defines recipes, how they compose and evaluate.
module Achille.Internal
( Cache
, emptyCache
, toCache
, fromCache
, fromContext
, MustRun(..)
, Context(..)
, Recipe(..)
, Task
, runRecipe
, nonCached
) where
import Prelude hiding (fail, liftIO)
import Data.Binary (Binary, encode, decodeOrFail)
import Data.Maybe (fromMaybe)
import Data.Functor (void)
import Control.Monad (ap)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Control.Monad.Fail (MonadFail, fail)
import Control.Applicative (liftA2)
import Data.Time.Clock (UTCTime)
import Data.ByteString.Lazy (ByteString, empty)
import Data.Bifunctor (first, second)
import System.FilePath.Glob (Pattern)
-- | A cache is a lazy bytestring.
type Cache = ByteString
-- | The empty cache.
emptyCache :: Cache
emptyCache = empty
-- | Cache a value.
toCache :: Binary a => a -> Cache
toCache = encode
-- | Retrieve a value from cache.
fromCache :: Binary a => Cache -> Maybe a
fromCache cache =
case decodeOrFail cache of
Left _ -> Nothing
Right (_, _, x) -> Just x
-- | Local rules for running a recipe
data MustRun = MustRunOne -- ^ The current recipe, and only this one, must run
| MustRunAll -- ^ All subsequent recipes must run
| NoMust -- ^ No obligation, the current recipe will be run as normal
deriving (Eq)
lowerMustRun :: MustRun -> MustRun
lowerMustRun MustRunAll = MustRunAll
lowerMustRun x = NoMust
-- | Try to load a value from the cache,
-- while respecting the rule for running the recipe.
-- That is, if the rule must run, nothing will be returned.
-- We also lower the run rule in the returned context, if possible.
--
-- The types are not explicit enough, should rewrite.
fromContext :: Binary a => Context b -> (Maybe a, Context b)
fromContext c =
let r = mustRun c in
if r /= NoMust then (Nothing, c {mustRun = lowerMustRun r})
else (fromCache (cache c), c)
-- | Description of a computation producing a value b given some input a.
newtype Recipe m a b = Recipe (Context a -> m (b, Cache))
-- | Context in which a recipe is being executed.
data Context a = Context
{ inputDir :: FilePath -- ^ Input root directory
, outputDir :: FilePath -- ^ Output root directory
, currentDir :: FilePath -- ^ Current directory
, timestamp :: UTCTime -- ^ Timestamp of the last run
, forceFiles :: [Pattern] -- ^ Files marked as dirty
, mustRun :: MustRun -- ^ Whether the current task must run
, cache :: Cache -- ^ Local cache
, inputValue :: a -- ^ Input value
} deriving (Functor)
-- | A task is a recipe with no input
type Task m = Recipe m ()
-- | Make a recipe out of a computation that is known not to be cached.
nonCached :: Functor m => (Context a -> m b) -> Recipe m a b
nonCached f = Recipe \c -> (, emptyCache) <$> f c {cache = emptyCache}
-- | Run a recipe with a given context.
runRecipe :: Recipe m a b -> Context a -> m (b, Cache)
runRecipe (Recipe r) = r
instance Functor m => Functor (Recipe m a) where
fmap f (Recipe r) = Recipe \c -> first f <$> r c
instance Monad m => Applicative (Recipe m a) where
pure = Recipe . const . pure . (, emptyCache)
(<*>) = ap
splitCache :: Cache -> (Cache, Cache)
splitCache = fromMaybe (emptyCache, emptyCache) . fromCache
instance Monad m => Monad (Recipe m a) where
Recipe r >>= f = Recipe \c -> do
let (cr, cf) = splitCache (cache c)
(x, cr') <- r c {cache = cr}
(y, cf') <- runRecipe (f x) c {cache = cf}
pure (y, toCache (cr', cf'))
-- parallelism for free?
Recipe r >> Recipe s = Recipe \c -> do
let (cr, cs) = splitCache (cache c)
(_, cr') <- r c {cache = cr}
(y, cs') <- s c {cache = cs}
pure (y, toCache (cr', cs'))
instance MonadIO m => MonadIO (Recipe m a) where
liftIO = nonCached . const . liftIO
instance MonadFail m => MonadFail (Recipe m a) where
fail = Recipe . const . fail
instance (Monad m, Semigroup b) => Semigroup (Recipe m a b) where
x <> y = liftA2 (<>) x y
instance (Monad m, Monoid b) => Monoid (Recipe m a b) where
mempty = pure mempty