{-# LANGUAGE ExistentialQuantification #-} {- | Process chunks of data in the IO monad. Typical inputs are strict storable vectors and piecewise constant values, and typical outputs are strict storable vectors. You may also combine several of these types using the Zip type constructor. We may substitute IO by ST in the future, but I am uncertain about that. On the one hand, the admissible IO functionality is very restricted, only memory manipulation is allowed, on the other hand we use ForeignPtrs that are not part of ST framework. -} module Synthesizer.CausalIO.Process ( T(Cons), fromCausal, mapAccum, traverse, runCont, runStorableChunkyCont, zip, continue, continueChunk, ) where import qualified Synthesizer.Causal.Process as Causal import qualified Synthesizer.Generic.Signal as SigG import qualified Synthesizer.Generic.Cut as CutG import qualified Synthesizer.Zip as Zip import qualified Data.StorableVector.Lazy as SVL import qualified Data.StorableVector as SV import Foreign.Storable (Storable, ) import qualified Control.Monad.Trans.State as MS import qualified Control.Arrow as Arr import qualified Control.Category as Cat import Control.Arrow ((^<<), (&&&), ) import Control.Monad (mplus, ) import Data.Monoid (Monoid, mempty, mappend, ) import System.IO.Unsafe (unsafePerformIO, unsafeInterleaveIO, ) import Prelude hiding (zip, ) {- Like the Causal arrow but unlike the Causal arrow from @synthesizer-llvm@, we are not using a parameter type @p@. In order to parameterize the process, you simply use a plain Haskell function, i.e. @p -> T a b@. This way, we do not need the Parameter type from @synthesizer-llvm@. However, the internal state type can depend on the value of parameters. This may be an advantage or a disadvantage, I do not know. -} data T a b = forall state. Cons {- If the transition function returns a chunk that is shorter than the input, then this is the last chunk. This way we do not need a MaybeT IO. -} (a -> state -> IO (b, state)) (IO state) {- The delete function must not do anything serious, e.g. close files, because it might not be called. Something like 'touchForeignPtr' is reasonable. -} (state -> IO ()) instance Cat.Category T where id = Arr.arr id (Cons nextB createB deleteB) . (Cons nextA createA deleteA) = Cons (\a (sa0,sb0) -> do (b,sa1) <- nextA a sa0 (c,sb1) <- nextB b sb0 return (c,(sa1,sb1))) (do sa <- createA sb <- createB return (sa,sb)) (\(sa,sb) -> deleteA sa >> deleteB sb) instance Arr.Arrow T where arr f = Cons (\ a () -> return (f a, ())) (return ()) (\ () -> return ()) first (Cons next create delete) = Cons (\(b,d) sa0 -> do (c,sa1) <- next b sa0 return ((c,d), sa1)) create delete fromCausal :: (Monoid b) => Causal.T a b -> T a b fromCausal (Causal.Cons next start) = Cons (\a s0 -> return $ case MS.runStateT (next a) s0 of Nothing -> (mempty, s0) Just (b,s1) -> (b,s1)) (return $ start) (\ _ -> return ()) mapAccum :: (a -> state -> (b, state)) -> state -> T a b mapAccum next start = Cons (\a s -> return $ next a s) (return start) (\ _ -> return ()) {- The parameter order is chosen this way, because the 'next' function definition might be large and can be separated with a ($). -} traverse :: state -> (a -> MS.State state b) -> T a b traverse start next = Cons (\a s -> return $ MS.runState (next a) s) (return start) (\ _ -> return ()) {- | This function converts a process into a function on lazy storable vectors. To this end it must call unsafePerformIO, that is, the effects of all functions called in the process must not be observable. I am not sure, we need this function at all. -} runCont :: (CutG.Transform a, CutG.Transform b) => T a b -> IO (([a] -> [b]) -> [a] -> [b]) runCont (Cons next create delete) = return $ \ procRest sig -> unsafePerformIO $ do let go xt s0 = unsafeInterleaveIO $ case xt of [] -> delete s0 >> return [] x:xs -> do (y,s1) <- next x s0 (if CutG.length y > 0 then fmap (y:) else id) $ (if CutG.length y < CutG.length x then return $ procRest $ CutG.drop (CutG.length y) x : xs else go xs s1) go sig =<< create {- | The same restrictions as for 'runCont' apply. -} runStorableChunkyCont :: (Storable a, Storable b) => T (SV.Vector a) (SV.Vector b) -> IO ((SVL.Vector a -> SVL.Vector b) -> SVL.Vector a -> SVL.Vector b) runStorableChunkyCont proc = flip fmap (runCont proc) $ \f cont -> SVL.fromChunks . f (SVL.chunks . cont . SVL.fromChunks) . SVL.chunks zip :: (Arr.Arrow arrow) => arrow a b -> arrow a c -> arrow a (Zip.T b c) zip ab ac = uncurry Zip.Cons ^<< ab &&& ac {- | @mappend@ should be used sparingly. In a loop it will have to construct types at runtime which is rather expensive. -} instance (CutG.Transform a, CutG.Read b, Monoid b) => Monoid (T a b) where mempty = Cons (\ _a () -> return (mempty, ())) (return ()) (\() -> return ()) mappend (Cons nextX createX deleteX) (Cons nextY createY deleteY) = Cons (\a s -> case s of Left s0 -> do (b1,s1) <- nextX a s0 let lenA = CutG.length a lenB = CutG.length b1 case compare lenA lenB of LT -> error "CausalIO.Process.mappend: output chunk is larger than input chunk" EQ -> return (b1, Left s1) GT -> do deleteX s1 (b2,s2) <- nextY (CutG.drop lenB a) =<< createY return (mappend b1 b2, Right s2) Right s0 -> do (b1,s1) <- nextY a s0 return (b1, Right s1)) (fmap Left createX) (either deleteX deleteY) data State a b = forall state. State (a -> state -> IO (b, state)) (state -> IO ()) state -- the only difference to (T a b) is the IO forceMaybe :: (Maybe a -> b) -> Maybe a -> b forceMaybe f ma = case ma of Nothing -> f Nothing Just a -> f $ Just a {- | If the first process does not produce any output, then the continuing process will not be started. -} continue :: (CutG.Transform a, SigG.Transform sig b) => T a (sig b) -> (b -> T a (sig b)) -> T a (sig b) continue (Cons nextX createX deleteX) procY = Cons (\a s -> case s of Left (lastB0, s0) -> do (b1,s1) <- nextX a s0 let lenA = CutG.length a lenB = CutG.length b1 lastB1 = mplus (fmap snd $ SigG.viewR b1) lastB0 cont lastB = (b1, Left (lastB,s1)) case compare lenA lenB of LT -> error "CausalIO.Process.continue: output chunk is larger than input chunk" EQ -> return $ forceMaybe cont lastB1 GT -> case lastB1 of Nothing -> return (mempty, Left (lastB1,s1)) Just lastB -> case procY lastB of Cons nextY createY deleteY -> do deleteX s1 (b2,s2) <- nextY (CutG.drop lenB a) =<< createY return (mappend b1 b2, Right (State nextY deleteY s2)) Right (State nextY deleteY s0) -> do (b1,s1) <- nextY a s0 return (b1, Right (State nextY deleteY s1))) (do sa <- createX return (Left (Nothing, sa))) (\s -> case s of Left (_lastB,s0) -> deleteX s0 Right (State _ deleteY s0) -> deleteY s0) {- | Pass the last non-empty output chunk as parameter to the continuing process. This breaks the abstraction from the chunk sizes, but we need it for implementing vectorized processing. -} continueChunk :: (CutG.Transform a, CutG.Transform b) => T a b -> (b -> T a b) -> T a b continueChunk (Cons nextX createX deleteX) procY = Cons (\a s -> case s of Left (lastB0, s0) -> do (b1,s1) <- nextX a s0 let lenA = CutG.length a lenB = CutG.length b1 cont lastB = (b1, Left (lastB,s1)) case compare lenA lenB of LT -> error "CausalIO.Process.continueChunk: output chunk is larger than input chunk" EQ -> -- force the decision on lenB, otherwise thunks will accumulate return $ if lenB==0 then cont lastB0 else cont b1 GT -> if lenB==0 then return $ cont lastB0 else case procY b1 of Cons nextY createY deleteY -> do deleteX s1 (b2,s2) <- nextY (CutG.drop lenB a) =<< createY return (mappend b1 b2, Right (State nextY deleteY s2)) Right (State nextY deleteY s0) -> do (b1,s1) <- nextY a s0 return (b1, Right (State nextY deleteY s1))) (do sa <- createX return (Left (mempty, sa))) (\s -> case s of Left (_lastB,s0) -> deleteX s0 Right (State _ deleteY s0) -> deleteY s0)