{-# LANGUAGE ScopedTypeVariables #-} -- | -- Module : Control.Auto.Serialize -- Description : Serializing and deserializing 'Auto's to and from disk, -- and also 'Auto' transformers focused around serialization. -- Copyright : (c) Justin Le 2015 -- License : MIT -- Maintainer : justin@jle.im -- Stability : unstable -- Portability : portable -- -- This module provides tools for working with the automatically derived -- serializability and resumability of 'Auto's. The first half contains -- boring wrappers around encoding and decoding to and from binary, -- filepaths on disk, etc. -- -- The second half contains 'Auto' transformers that "imbue" an 'Auto' with -- IO serialization abilities. Note that these all require an underlying -- 'Monad' that is an instance of 'MonadIO'. -- -- You have "identity-like" transformers that take an 'Auto' and spit it -- back out operationally unchanged...but every step, it might do some -- behind-the-scenes saving or re-load itself from disk when it is first -- stepped. Or you have some "trigger enhancers" that take normal 'Auto's -- and give you the ability to "trigger" saving and loading events on the -- 'Auto' using the 'Blip' mechanisms and blip stream semantics from -- "Control.Auto.Blip". -- -- Note that the entire 'Auto' construct is a little bit awkward when it -- comes to performing IO effects --- it isn't exactly what they were -- designed for originally. Hooking on effects to stepping can be -- powerful, but as of now, not much has been looked into meaningful error -- handling when working with IO. If you have any experience with this and -- are willing to help, please feel free to send me an e-mail or open an -- issue on the <https://github.com/mstksg/auto/issues issue tracker>! -- module Control.Auto.Serialize ( -- * Serializing and deserializing 'Auto's -- ** To and from "Data.Serialize" types saveAuto , resumeAuto -- ** To and from binary , encodeAuto , decodeAuto -- ** To and from disk , writeAuto , readAuto , readAutoDef -- * Imbuing 'Auto's with serialization -- ** Implicit automatic serialization , saving , loading' , loading , serializing' , serializing -- ** Triggered (blip stream-based) automatic serialization -- $onfrom -- *** External triggering , saveOnB , loadOnB' , loadOnB -- *** Intrinsic triggering , saveFromB , loadFromB' , loadFromB ) where import Control.Applicative import Control.Auto.Blip.Internal import Control.Auto.Core import Control.Exception import Control.Monad import Control.Monad.IO.Class import Prelude import System.IO.Error import qualified Data.ByteString as B -- | Give a 'FilePath' and an 'Auto', and 'readAuto' will attempt to resume -- the saved state of the 'Auto' from disk, reading from the given -- 'FilePath'. Will return 'Left' upon a decoding error, with the error, -- and 'Right' if the decoding is succesful. readAuto :: FilePath -- ^ filepath to read from -> Auto m a b -- ^ 'Auto' to resume -> IO (Either String (Auto m a b)) readAuto fp a = decodeAuto a <$> B.readFile fp -- | Like 'readAuto', but will return the /original/ 'Auto' (instead of -- a resumed one) if the file does not exist. -- -- Useful if you want to "resume an 'Auto'" "if there is" a save state, or -- just use it as-is if there isn't. readAutoDef :: FilePath -- ^ filepath to read from -> Auto m a b -- ^ 'Auto' to resume -> IO (Either String (Auto m a b)) readAutoDef fp a = do esa <- try (readAuto fp a); case esa of Right a' -> return a' Left e | isDoesNotExistError e -> return (Right a) | otherwise -> throw e -- | Like 'readAuto', but will throw a runtime exception on a failure to -- decode or an IO error. readAutoErr :: FilePath -- ^ filepath to read from -> Auto m a b -- ^ 'Auto' to resume -> IO (Auto m a b) readAutoErr fp a = do esa <- readAuto fp a return $ case esa of Left e -> error $ "readAutoErr: Corrupted Auto binary -- " ++ e Right a' -> a' -- | Given a 'FilePath' and an 'Auto', serialize and freeze the state of -- the 'Auto' as binary to that 'FilePath'. writeAuto :: FilePath -- ^ filepath to write to -> Auto m a b -- ^ 'Auto' to serialize -> IO () writeAuto fp a = B.writeFile fp (encodeAuto a) -- | "Transforms" the given 'Auto' into an 'Auto' that constantly saves its -- state to the given 'FilePath' at every "step". Requires an underlying -- 'MonadIO'. -- -- Note that (unless the 'Auto' depends on IO), the resulting 'Auto' is -- meant to be operationally /identical/ in its inputs/outputs to the -- original one. -- saving :: MonadIO m => FilePath -- ^ filepath to write to -> Auto m a b -- ^ 'Auto' to transform -> Auto m a b saving fp = interceptO $ \(y, a') -> do liftIO $ writeAuto fp a' return y -- | "Transforms" the given 'Auto' into an 'Auto' that, when you /first/ -- try to run or step it, "loads" itself from disk at the given 'FilePath'. -- -- Will throw a runtime exception on either an I/O error or a decoding -- error. -- -- Note that (unless the 'Auto' depends on IO), the resulting 'Auto' is -- meant to be operationally /identical/ in its inputs/outputs to the -- /fast-forwarded/ original 'Auto'. -- loading :: MonadIO m => FilePath -- ^ filepath to read from -> Auto m a b -- ^ 'Auto' to transform -> Auto m a b loading fp a0 = mkAutoM (loading fp <$> resumeAuto a0) (saveAuto a0) $ \x -> do a <- liftM loaded . liftIO $ readAutoErr fp a0 stepAuto a x where loaded a = mkAutoM (loading' fp <$> resumeAuto a) (saveAuto a) $ \x -> do (y, a') <- stepAuto a x return (y, loaded a') -- | Like 'loading', except silently suppresses all I/O and decoding -- errors; if there are errors, it returns back the given 'Auto' as-is. -- -- Useful for when you aren't sure the save state is on disk or not yet, -- and want to resume it only in the case that it is. loading' :: MonadIO m => FilePath -- ^ filepath to read from -> Auto m a b -- ^ 'Auto' to transform (or return unchanged) -> Auto m a b loading' fp a0 = mkAutoM (loading' fp <$> resumeAuto a0) (saveAuto a0) $ \x -> do a <- do ea' <- liftIO $ readAutoDef fp a0 case ea' of Right a' -> return (loaded a') Left _ -> return a0 stepAuto a x where loaded a = mkAutoM (loading' fp <$> resumeAuto a) (saveAuto a) $ \x -> do (y, a') <- stepAuto a x return (y, loaded a') -- | A combination of 'saving' and 'loading'. When the 'Auto' is first -- run, it loads the save state from the given 'FilePath' and fast forwards -- it. Then, subsequently, it updates the save state on disk on every -- step. serializing :: MonadIO m => FilePath -- ^ filepath to read from and write to -> Auto m a b -- ^ 'Auto' to transform -> Auto m a b serializing fp a = loading fp (saving fp a) -- | Like 'serializing', except suppresses all I/O and decoding errors. -- -- Useful in the case that when the 'Auto' is first run and there is no -- save state yet on disk (or the save state is corrupted), it'll "start -- a new one"; if there is one, it'll load it automatically. Then, on -- every further step in both cases, it'll update the save state. serializing' :: MonadIO m => FilePath -- ^ filepath to read from and write to -> Auto m a b -- ^ 'Auto' to transform -> Auto m a b serializing' fp a = loading' fp (saving fp a) -- $onfrom -- -- Note that these follow the naming conventions from -- "Control.Auto.Switch": Something "from" a blip stream is a thing -- triggered by the 'Auto' itself, and something "on" a blip stream is -- a thing triggered externally, from another 'Auto'. -- | Takes an 'Auto' that produces a blip stream with a 'FilePath' and -- a value, and turns it into an 'Auto' that, outwardly, produces just the -- value. -- -- Whenever the output blip stream emits, it automatically serializes and -- saves the state of the 'Auto' to the emitted 'FilePath'. -- -- In practice, this allows any 'Auto' to basically control when it wants -- to "save", by providing a blip stream. -- -- The following is an alternative implementation of 'saving', except -- saving every two steps instead of every step: -- -- @ -- saving2 fp a = 'saveFromB' (a '&&&' ('every' 2 . 'pure' fp)) -- @ -- -- Or, in proc notation: -- -- > saving2 fp a = saveFromB $ proc x -> do -- > y <- a -< x -- > b <- every 2 -< fp -- > id -< (y, b) -- -- (Recall that @'every' n@ is the "Auto" that emits the received value -- every @n@ steps) -- -- In useful real-world cases, you can have the 'Auto' decide whether or -- not to save itself based on its input. Like, for example, when it -- detects a certain user command, or when the user has reached a given -- location. -- -- The following takes a 'FilePath' and an 'Auto' (@a@), and turns it into -- an 'Auto' that "saves" whenever @a@ crosses over from positive to -- negative. -- -- @ -- saveOnNegative fp a = saveFromB $ proc x -> do -- y <- a -< x -- saveNow <- 'became' (< 0) -< y -- id -< (y, fp '<$' saveNow) -- @ -- -- Contrast to 'saveOnB', where the saves are triggered by outside input. -- In this case, the saves are triggered by the 'Auto' to be saved itself. -- saveFromB :: MonadIO m => Auto m a (b, Blip FilePath) -- ^ 'Auto' producing a value -- @b@ and a blip stream -- with a 'FilePath' to save -- to -> Auto m a b saveFromB = interceptO $ \((y, b), a') -> do case b of Blip p -> liftIO $ writeAuto p a' _ -> return () return y -- | Takes an 'Auto' that outputs a @b@ and a blip stream of 'FilePath's -- and returns an 'Auto' that ouputs only that @b@ stream...but every time -- the blip stream emits, it "resets/loads" itself from that 'FilePath'. -- -- The following is a re-implementation of 'loading'...except delayed by -- one (the second step that is run is the first "resumed" step). -- -- @ -- loading2 fp a = 'loadFromB' $ proc x -> do -- y <- a -< x -- loadNow <- 'immediately' -< fp -- 'id' -< (y, loadNow) -- @ -- -- (the blip stream emits only once, immediately, to re-load). -- -- In the real world, you could have the 'Auto' decide to reset or resume -- itself based on a user command: -- -- @ -- loadFrom = loadFromB $ proc x -> do -- steps <- count -< () -- toLoad <- case words x of -- ("load":fp:_) -> do -- immediately -< fp -- _ -> do -- never -< () -- id -< (steps, toLoad) -- @ -- -- This will throw a runtime error on an IO exception or parsing error. -- loadFromB :: MonadIO m => Auto m a (b, Blip FilePath) -- ^ 'Auto' with an output -- and a blip stream to -- trigger re-loading -- itself from the given -- filepath -> Auto m a b loadFromB a = mkAutoM (loadFromB' <$> resumeAuto a) (saveAuto a) $ \x -> do ((y, b), a') <- stepAuto a x a'' <- case b of Blip p -> liftIO $ readAutoErr p a' NoBlip -> return a' return (y, loadFromB' a'') -- | Like 'loadFromB', except silently ignores errors. When a load is -- requested, but there is an IO or parse error, the loading is skipped. loadFromB' :: MonadIO m => Auto m a (b, Blip FilePath) -- ^ 'Auto' with an output -- and a blip stream to -- trigger re-loading -- itself from the given -- filepath -> Auto m a b loadFromB' a0 = mkAutoM (loadFromB' <$> resumeAuto a0) (saveAuto a0) $ \x -> do ((y, b), a1) <- stepAuto a0 x a2 <- case b of Blip p -> do ea3 <- liftIO $ readAutoDef p a1 case ea3 of Right a3 -> return a3 Left _ -> return a1 NoBlip -> return a1 return (y, loadFromB' a2) -- | Takes an 'Auto' and basically "wraps" it so that you can trigger saves -- with a blip stream. -- -- For example, we can take @'sumFrom' 0@: -- -- @ -- 'saveOnB' ('sumFrom' 0) :: 'Auto' 'IO' ('Int', 'Blip' 'FilePath') 'Int' -- @ -- -- It'll behave just like @'sumFrom' 0@ (with the input you pass in the -- first field of the tuple)...and whenever the blip stream (the second -- field of the input tuple) emits, it'll save the state of @'sumFrom' 0@ -- to disk at the given 'FilePath'. -- -- Contrast to 'saveFromB', where the 'Auto' itself can trigger saves; in -- this one, saves are triggered "externally". -- -- Might be useful in similar situations as 'saveFromB', except if you want -- to trigger the save externally. -- saveOnB :: MonadIO m => Auto m a b -- ^ 'Auto' to make saveable-by-trigger -> Auto m (a, Blip FilePath) b saveOnB a = mkAutoM (saveOnB <$> resumeAuto a) (saveAuto a) $ \(x, b) -> do case b of Blip p -> liftIO $ writeAuto p a NoBlip -> return () (y, a') <- stepAuto a x return (y, saveOnB a') -- | Takes an 'Auto' and basically "wraps" it so that you can trigger -- loads/resumes from a file with a blip stream. -- -- For example, we can take @'sumFrom' 0@: -- -- @ -- 'loadOnB' ('sumFrom' 0) :: 'Auto' 'IO' ('Int', 'Blip' 'FilePath') 'Int' -- @ -- -- It'll behave just like @'sumFrom' 0@ (with the input you pass in the -- first field of the tiple)...and whenever the blip stream (the second -- field of the input tuple) emits, it'll "reset" and "reload" the -- @'sumFrom' 0@ from the 'FilePath' on disk. -- -- Will throw a runtime exception if there is an IO error or a parse error. -- -- Contrast to 'loadFromB', where the 'Auto' itself can trigger -- reloads/resets; in this one, the loads are triggered "externally". -- -- Might be useful in similar situations as 'loadFromB', except if you want -- to trigger the loading externally. -- loadOnB :: MonadIO m => Auto m a b -- ^ 'Auto' to make reloadable-by-trigger -> Auto m (a, Blip FilePath) b loadOnB a = mkAutoM (loadOnB' <$> resumeAuto a) (saveAuto a) $ \(x, b) -> do a' <- case b of Blip p -> liftIO $ readAutoErr p a NoBlip -> return a (y, a'') <- stepAuto a' x return (y, loadOnB' a'') -- | Like 'loadOnB', except silently ignores errors. When a load is -- requested, but there is an IO or parse error, the loading is skipped. loadOnB' :: MonadIO m => Auto m a b -- ^ 'Auto' to make reloadable-by-trigger -> Auto m (a, Blip FilePath) b loadOnB' a0 = mkAutoM (loadOnB' <$> resumeAuto a0) (saveAuto a0) $ \(x, b) -> do a1 <- case b of Blip p -> do ea2 <- liftIO $ readAutoDef p a0 case ea2 of Right a2 -> return a2 Left _ -> return a0 NoBlip -> return a0 (y, a2) <- stepAuto a1 x return (y, loadOnB' a2)