Dflow (empty) → 0.0.1
raw patch · 7 files changed
+1207/−0 lines, 7 filesdep +HUnitdep +QuickCheckdep +basesetup-changed
Dependencies added: HUnit, QuickCheck, base, containers, stm, test-framework, test-framework-quickcheck2, time
Files
- Dflow.cabal +51/−0
- LICENSE.txt +26/−0
- README.txt +82/−0
- Setup.hs +2/−0
- src/Control/RTSP.hs +666/−0
- test/ArbTest.hs +340/−0
- test/Main.hs +40/−0
+ Dflow.cabal view
@@ -0,0 +1,51 @@+name: Dflow+version: 0.0.1+cabal-version: >= 1.10+build-type: Simple+license: BSD3+license-file: LICENSE.txt+copyright: © Paul Johnson 2012+author: Paul Johnson+maintainer: <paul@cogito.org.uk>+category: Reactivity+tested-with: GHC==7.0.4+synopsis: Processing Real-time event streams+data-files: README.txt+description: This library provides Real Time Stream Processors (RTSPs). An RTSP+ transforms an input event stream into an output event stream. The output+ events occur asynchronously with input events. RTSPs can be composed into + pipelines or executed in parallel and their outputs merged. A Real Time+ Action (RTA) monad is provided for creating new primitive RTSPs.+stability: Experimental++library+ hs-source-dirs: src+ build-depends: + base >= 4 && < 5,+ time >=1.1,+ stm,+ QuickCheck >= 2.4,+ containers >= 0.4+ exposed-modules: Control.RTSP+ ghc-options: -fspec-constr-count=6+ default-language: Haskell2010++test-suite ArbTest+ type: exitcode-stdio-1.0+ x-uses-tf: true+ build-depends: + base >= 4,+ HUnit >= 1.2 && < 2,+ QuickCheck >= 2.4,+ test-framework >= 0.4.1,+ test-framework-quickcheck2+ hs-source-dirs: src, test+ ghc-options: -Wall -threaded -rtsopts=all -fspec-constr-count=10+ -- Add "-fhpc" to the previous line to enable test coverage+ default-language: Haskell2010+ other-modules: + ArbTest,+ Control.RTSP,+ Main++ main-is: Main.hs
+ LICENSE.txt view
@@ -0,0 +1,26 @@+Copyright (c) 2012, Paul Johnson+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++ Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ Redistributions in binary form must reproduce the above copyright+ notice, this list of conditions and the following disclaimer in+ the documentation and/or other materials provided with the+ distribution.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.txt view
@@ -0,0 +1,82 @@+Dflow+=====++Real time event processing using data flow declarations.++The following has been tested using ghc 7.0.4 under Fedora 15 64-bit.++Installation+------------++$ cabal configure+$ cabal build+$ cabal install+++Tests+-----++$ cabal configure --enable-test+$ cabal build+$ cabal test++The tests work by building random networks of event processors and+checking them against an equivalent list implementation. Occasionally+they seem to hit a pathological case that chews up memory and CPU+time. If memory usage goes over 1GB then kill the test and try again.++Test Coverage+-------------++Edit the Dflow.cabal file and add "-fhpc" to the GHC options for the+test (see comment in the file). Then do "cabal clean" and follow the+instructions for "Tests" above. Then do:++$ hpc markup ArbTest.tix --destdir=coverage++The coverage report is in coverage/hpc_index.html.++Concepts+--------++The main data types for the application programmer are:++Event: A value that occurs at a certain time. For instance an +"Event Char" might represent a key press.++RTSP: The Real Time Stream Processor. A value of type "RTSP x y"+takes in events of type "x" and emits events of type "y". RTSPs can+be strung together into pipelines using "." (or ">>>" if you prefer+your data to flow left-to-right). RTSPs are also Monoids, so you can+fork your data through two parallel RTSPs and then merge the results.++RTA: A monad for building stateful RTSPs. Convert an RTA into an+RTSP using "execRTA" or "accumulateRTA" depending what you want to+do with pending output events when a new input event arrives.+++You can test an RTSP in "fast time" (that is, without waiting for+real-time delays) by using "simulateRTSP". However the argument list+of input events must be finite. Then you can execute the RTSP in real+time using "execRTSP" and be confident that the real time behaviour+will match the fast-time behaviour.++To Do+-----++Events are currently sorted into time order using (in effect) an+O(n) insertion sort. Event streams should use something more+sophisticated internally, such as a heap.++What API, if any, should be exposed for event streams? On one hand +developers should be able to create new kinds of RTSP primitives+using event streams, but on the other hand anything you can do with +event streams can be done using RTA anyway.++Figure out some way for RTAs and RTSPs to execute non-blocking IO with+timeouts, probably using continuations in some way. Hence develop true+distribution by having RTSPs on different machines interact in a+declarative manner.++Make state persistent, together with a replay mechanism for lost+events.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ src/Control/RTSP.hs view
@@ -0,0 +1,666 @@+{- |++Real Time Stream Processors are used to describe pipelines that process events in real time. An event is described as a+(Time, Value) pair. When an RTSP receives an event it can respond by emitting zero or more events at any time at or after the+receipt of the original event. Further incoming events may influence the stream of emitted events.++Piplelines of RTSPs can be built up using the '(.)' operator from the Category instance, or alternatively the '(>>>)' operator +(which is merely dot with its arguments swapped). RTSPs can be run in parallel with their outputs merged using "mappend" +from the Monoid instance. ++Within the RTSP implementation all notions of \"delay\" and \"time\" merely refer to the time component of events, and are used+for event ordering. Only the 'execRTSP' function, which runs in the IO monad, executes any actual real-time delay.++The main data types for the application programmer are:++['Event'] A value that occurs at a certain time. For instance an @'Event' Char@ might represent a key press.++['RTSP'] The Real Time Stream Processor. A value of type @'RTSP' x y@ takes in events of type @x@ and emits events of type @y@. +RTSPs can be strung together into pipelines using @(.)@ (or @(>>>)@ if you prefer your data to flow left-to-right). RTSPs are also +monoids, so you can fork your data through two parallel RTSPs and then merge the results.++['RTA'] A monad for building stateful RTSPs. Convert an 'RTA' into an 'RTSP' using 'execRTA' or 'accumulateRTA' depending what you +want to do with pending output events when a new input event arrives.++You can test an RTSP in \"fast time\" (that is, without waiting for real-time delays) by using 'simulateRTSP'. Then you can +execute the RTSP in real time using 'execRTSP' and be confident that the real time behaviour will match the fast-time behaviour.++/Simultaneous Events/++The handling of logically simultaneous events in discrete event simulation is a long-standing problem. The three basic approaches+are:++1. Impose an arbitrary but deterministic order on \"simultaneous\" events.++2. Collect the simultaneous events and pass them to the application, on the basis that the application programmer can then+ impose the appropriate semantics.++3. Simulate all possible orderings.++This library takes the first approach. Option 2 would force each RTSP to wait for the next event to see if it was+simultaneous, which is possible in a simulator but not in a real time system. In a real time system option 3 is not feasible,+and would still leave the problem of which ordering to present to the outside world as the \"real\" one.++When two simultaneous events arrive at an RTSP, the current implementation uses the following rules: ++* Simultaneous output events retain the order of the input events that triggered them. Hence simultaneous events never \"overtake\".++* In the case of @(id \`mappend\` stream (+ 1))@ the output alternates the left and right expressions, starting with the left.++However these properties interact in ways that are complex, hard to define formally and not guaranteed to be stable. Code+that depends on the ordering of simultaneous events should therefore be avoided.+-}++module Control.RTSP (+ -- ** Events+ Event (..),+ isBefore,+ -- ** Real Time Stream Processors+ EventStream,+ emitsBefore,+ nullStream,+ esFinished,+ esPeek,+ esFutures,+ esProcess,+ esMerge,+ RTSP (..),+ simulateRTSP,+ execRTSP,+ stream,+ accumulate,+ -- ** Manipulating event times+ repeatEvent,+ delay0,+ delay,+ -- ** Conditional event processing+ Cond,+ streamFilter,+ cond,+ cond1,+ ifThenElse,+ -- ** Real Time Actions with state+ RTA,+ get,+ put,+ modify,+ emit,+ pause,+ now,+ execRTA,+ accumulateRTA,+) where++++import Control.Category+import Control.Concurrent+import Control.Concurrent.STM+import Control.Monad+import Data.List+import Data.Monoid+import Data.Sequence+import Data.Time++import Prelude hiding ((.),id, repeat)+++infix 4 `isBefore`, `emitsBefore`++++-- | Real time events.+data Event a = Event {eventTime :: UTCTime, eventValue :: a} + deriving (Show, Eq)++instance Functor Event where+ fmap f (Event t v) = Event t (f v)+++-- | True if the first event occurs strictly before the second. This makes @Event@ a poset (partially ordered set).+-- Infix priority 4 (the same as other comparison operators).+isBefore :: Event a -> Event b -> Bool+isBefore ev1 ev2 = eventTime ev1 < eventTime ev2+++{- | +A real-time event stream cannot be described without reference to+unknown future inputs. Hence @EventStream@ embodies two possible futures:++* An @Event c@ will be emitted at some time in the future, with a new+ @EventStream@ representing the future after that event. ++* An incoming @Event b@ will arrive before the next @Event c@ is emitted,+ creating a new @EventStream@ representing the response to that event. The+ old @Event c@ may or may not be part of the new @EventStream@.++There are also two degenerate cases:++* Wait: no event is scheduled to be emitted, and the @EventStream@ + just waits for an incoming event.+ +* Finish: no event will ever be emitted, regardless of incoming events.+ This is explicitly distinguished so that complex RTSP expressions+ can be GC'd if they can be proven to be finished.+ +Event streams are like the Mirror of Galadriel, for they show things that+were, things that are, and things that yet may be. But which it is that he+sees, even the wisest cannot always tell.++Seeing is both good and perilous. An event stream may be modified by+new events, but exceptions or inconsistent results will occur if the incoming+events are not in increasing order of time.+-}+data EventStream b c =+ Emit (Event c) (EventStream b c) (RTSP b c)+ -- ^ The next event to be emitted, the following EventStream, and the function to+ -- handle an incoming event before then.+ | Wait (RTSP b c) + -- ^ Degenerate case: no event scheduled to be emitted.+ | Finish + -- ^ Semantically equivalent to @Wait eventSink@, but allows completed streams to be GC'd.+ deriving (Show)+++-- | Peek at the events that will be emitted by this EventStream if no incoming event interrupts them.+esPeek :: EventStream b c -> [Event c]+esPeek (Emit ev es1 _) = ev : esPeek es1+esPeek _ = []+++-- | True if the first argument is scheduled to emit an event before the second. This makes @EventStream@ a poset +-- (partially ordered set). Infix priority 4.+emitsBefore :: EventStream b1 c1 -> EventStream b2 c2 -> Bool+emitsBefore Finish _ = False+emitsBefore (Wait _) _ = False+emitsBefore (Emit ev1 _ _) (Emit ev2 _ _) = ev1 `isBefore` ev2+emitsBefore (Emit _ _ _) _ = True+++-- Only events satisfying the predicate will be passed on.+esFilter :: (b -> Bool) -> EventStream b b+esFilter = Wait . rtspFilter+++-- | Only events satisfying the predicate will be passed on.+rtspFilter :: (b -> Bool) -> RTSP b b+rtspFilter p = RTSP $ \ev -> if p $ eventValue ev then Emit ev (esFilter p) (rtspFilter p) else esFilter p+++-- | All the possible futures of the event stream.+esFutures :: EventStream b c -> [(Event c, EventStream b c)]+esFutures (Emit e es1 _) = (e, es1) : esFutures es1+esFutures _ = []+++-- | True if the event stream is guaranteed not to emit any future events, regardless of input.+esFinished :: EventStream b c -> Bool+esFinished Finish = True+esFinished _ = False+++-- | Merge the outputs of two event streams. Input events are delivered+-- to both streams.+esMerge :: EventStream b c -> EventStream b c -> EventStream b c+esMerge Finish es = es+esMerge es Finish = es+esMerge (Wait k1) (Wait k2) =+ Wait (splitRTSP k1 k2)+esMerge (Emit e es1 k1) es2@(Wait k2) =+ Emit e (esMerge es1 es2) (splitRTSP k1 k2)+esMerge es1@(Wait k1) (Emit e es2 k2) =+ Emit e (esMerge es1 es2) (splitRTSP k1 k2)+esMerge es1@(Emit e1 es1a k1) es2@(Emit e2 es2a k2) =+ if e2 `isBefore` e1+ then Emit e2 (esMerge es1 es2a) (splitRTSP k1 k2)+ else Emit e1 (esMerge es1a es2) (splitRTSP k1 k2)+++-- | Given a new input event to an existing event stream, this returns the modified event stream. When @esProcess@ +-- is called on the result the Event argument to the second call must not occur before the first (they can be+-- simultaneous). More formally, if+--+-- > esOut = esProcess (esProcess esIn ev1) ev2+-- +-- then @not (ev2 `isBefore` ev1)@. This precondition is not checked. +esProcess :: Event b -> EventStream b c -> EventStream b c+esProcess _ Finish = Finish+esProcess ev (Wait k) = runRTSP k ev+esProcess eIn (Emit eOut rest k) =+ if eIn `isBefore` eOut+ then runRTSP k eIn+ else Emit eOut (esProcess eIn rest) k++++-- | An event stream that never generates anything.+nullStream :: EventStream b c+nullStream = Finish+++-- | Real Time Stream Processor (RTSP)+-- +-- An EventStream cannot exist independently of some event that caused it to start. Hence the only way to+-- create an EventStream is through an RTSP.+-- +-- * "mempty" is the event sink: it never emits an event.+-- +-- * "mappend" runs its arguments in parallel and merges their outputs.+-- +-- * "id" is the null operation: events are passed through unchanged.+-- +-- * "(.)" is sequential composition: events emitted by the second argument are passed to the first argument.+newtype RTSP b c = RTSP {runRTSP :: Event b -> EventStream b c}+++instance Show (RTSP b c) where show _ = "-RTSP-"+++instance Monoid (RTSP b c) where+ mempty = eventSink+ mappend = splitRTSP+++instance Monoid (EventStream b c) where+ mempty = nullStream+ mappend = esMerge+++instance Functor (EventStream b) where+ fmap f (Emit eOut rest k) = Emit (fmap f eOut) (fmap f rest) (fmap f k)+ fmap f (Wait k) = Wait (fmap f k)+ fmap _ Finish = Finish+ + +instance Functor (RTSP b) where+ fmap f (RTSP r) = RTSP $ \evt -> fmap f $ r evt+ ++{-+The (.) operator for EventStream has to deal with several scenarios:++1: (Wait k2) . (Wait k1). This is simple because the only possible event+ is an input that is piped into k1. The result is the composition of the result with+ (Wait k2), achieived using the instance for RTSP.++2: (Wait k2) . es1@(Emit e1 es1a k1). In this case there are two timelines:+ a) e1 : (k2 e1) . es1a -- Event e1 is passed to k2 and the result composed with es1a.+ b) ev e1: (Wait k2) . (k1 ev) -- e1 never happens. Instead the input is passed to k1, which+ generates a new event stream to be composed with k2.++ The code for case b is the "k" value in the "let" clause.++3: es2@(Emit e2 es2a k2) es1@(Emit e1 es1a k1). The logic here is similar to scenario+ 2, except that the timing of e2 has to be taken into account as well. There are five basic+ timelines. Let ev = the next input event and es2b = runRTSP k2 e1++ a) e2 e1 : Emit e2 (es2a . es1a) (k2 . k1)+ b) e1 e2 : k2 e1 . es1a -- e2 never happens because it is overridden by e1+ c) ev ... : es2 . (k1 ev) -- ev overrides e1, and the new output is fed to es2.+ d) e1 ev e2 : esProcess (es2b . es1a) ev -- e1 overrides e2, giving es1a and es2b to process ev.+ e) e2 ev e1 : Emit e2 (es2a . (k1 ev)) -- e2 is emitted, then ev overrides e1.+-}+instance Category EventStream where+ -- id :: EventStream b c+ id = Wait id++ -- (.) :: EventStream c d -> EventStream b c -> EventStream b d+ Finish . _ = Finish+ (Wait _) . Finish = Finish+ (Emit e es1 _). Finish = let future = Emit e (es1 . Finish) (RTSP $ \_ -> future) in future + (Wait k2) . (Wait k1) = Wait $ k2 . k1+ es2@(Wait k2) . Emit e1 es1a k1 =+ let k = RTSP $ \ev ->+ if ev `isBefore` e1+ then es2 . runRTSP k1 ev -- Timeline 2b+ else esProcess ev es -- Timeline 2a+ es = (runRTSP k2 e1) . es1a -- Future if ev never happens+ in case es of+ Emit e2 es2b _ -> Emit e2 es2b k+ Wait _ -> Wait k+ Finish -> Wait k -- if ev `isBefore` e1 then es never happens.+ es2@(Emit e2 es2a _) . es1@(Wait k1) =+ Emit e2 (es2a . es1) (RTSP $ \ev -> es2 . runRTSP k1 ev)+ es2@(Emit e2 es2a k2) . es1@(Emit e1 es1a k1) =+ let + es = (runRTSP k2 e1) . es1a+ in if e1 `isBefore` e2+ then -- Timelines 3b, 3c and 3d. e2 never happens.+ let k = RTSP $ \ev ->+ if ev `isBefore` e1+ then es2 . runRTSP k1 ev -- Timeline 3c+ else esProcess ev es -- Timeline 3d+ in case es of+ Emit e3 es3 _ -> Emit e3 es3 k+ Wait _ -> Wait k+ Finish -> Wait k -- As above.+ else -- Timelines 3a, 3c and 3e.+ let k = RTSP $ \ev -> es2 . runRTSP k1 ev+ in Emit e2 (es2a . es1) k++++instance Category RTSP where+ -- id :: RTSP b c+ id = RTSP $ \ev -> Emit ev id id+ -- (.) :: RTSP c d -> RTSP b c -> RTSP b d+ r2 . r1 = RTSP $ \e0 -> (Wait r2) . runRTSP r1 e0++++-- | Execute an RTSP against a list of events. Useful for testing.+simulateRTSP :: RTSP b c+ -- ^ The processor to execute.+ -> [Event b]+ -- ^ The events must be finite and in chronological order. This is unchecked.+ -> [Event c]+simulateRTSP r = esPeek . foldl (flip esProcess) (Wait r)++++-- | Execute an RTSP in the IO monad. The function returns immediately with an action for pushing events into the RTSP.+execRTSP :: + RTSP b (IO ())+ -- ^ The output of the RTSP is a series of action events that will be executed in a separate thread sequentially at the + -- times given. The actions may, of course, fork their own threads as necessary.+ -- + -- execRTSP uses 'atomically', so it cannot be called within 'unsafePerformIO'.+ -> IO (b -> IO ())+execRTSP r = do+ eventQ <- newTChanIO+ let+ + putValue v = do+ t <- getCurrentTime+ atomically $ writeTChan eventQ $ Event t v+ + execStream (Emit ev es r1) = do+ {-+ "c1" and "c2" are threads that race to put a value in "var". "c1" waits until+ the next event emission time and then puts "Nothing". "c2" waits for the next+ input on "eventQ" and puts "Just" the event. Once this happens "mev2" can get+ the result and both threads are killed. + + The tricky bit is avoiding a race condition in "c2" where it reads the + channel and is then killed by the timeout, which would result in a dropped event. + "var" is never emptied, so if "c1" wins the race then "c2" can never complete + before being killed, so the event remains on the queue ready for the next race. + -}+ var <- newEmptyTMVarIO+ c1 <- timeout (eventTime ev) (atomically $ putTMVar var Nothing)+ c2 <- forkIO $ atomically $ do+ ev1 <- readTChan eventQ+ putTMVar var $ Just ev1++ mev2 <- atomically $ readTMVar var+ killThread c1+ killThread c2 + case mev2 of+ Just ev2 -> execStream $ runRTSP r1 ev2+ Nothing -> do+ () <- eventValue ev+ execStream es+ execStream (Wait r1) = do+ ev2 <- atomically $ readTChan eventQ+ execStream $ runRTSP r1 ev2+ execStream Finish = return ()+ + _ <- forkIO $ execStream $ Wait r+ return putValue+ + +-- Execute the given action at the given time. Returns immediately with the ThreadID that will execute the action. +timeout :: UTCTime -> IO () -> IO ThreadId+timeout t action = forkIO $ do+ t0 <- getCurrentTime+ longDelay (round ((t `diffUTCTime` t0) * 1000000))+ action + where+ -- threadDelay takes an Int, which may be as small as 2^29 (a bit over 5 minutes).+ longDelay :: Integer -> IO ()+ longDelay dt = + let (n,dt1) = if dt > 0 then dt `divMod` cycleT else (0,0)+ in do+ replicateM_ (fromIntegral n) $ threadDelay (fromIntegral cycleT)+ threadDelay (fromIntegral dt1) + cycleT = 500000000 -- 500 seconds in uSec. Small enough to fit into an Int.+++-- | An RTSP that never emits events regardless of its inputs.+eventSink :: RTSP b c+eventSink = RTSP $ \_ -> nullStream+++-- | A pure function converted into a stream processor+stream :: (b -> c) -> RTSP b c+stream f = fmap f id++++-- | Deliver an event to two stream processors and merge the resulting event+-- streams.+splitRTSP :: RTSP b c -> RTSP b c -> RTSP b c+splitRTSP (RTSP r1) (RTSP r2) = RTSP $ \evt -> esMerge (r1 evt) (r2 evt)+ where++++-- | Convert an list of events into an event stream. Events coming into this+-- stream are ignored. The list must be in chronological order.+streamFromList :: [Event c]-> EventStream b c+streamFromList [] = Finish+streamFromList (e:es) =+ Emit e (streamFromList es) (RTSP $ \_ -> streamFromList (e:es))+++-- | When a new input event is delivered to an RTSP it causes any future output events to be dropped in favour of the new+-- events. @accumulate@ instead keeps the events from previous inputs interleaved with the new ones. If you use+-- this unnecessarily then you will get duplicated events.+-- +-- If there are @n@ output events due to be emitted before an input event then this will require O(n) time for the input.+accumulate :: RTSP b c -> RTSP b c+accumulate r = rAccum [] r+ where+ rAccum evs@(ev1:evs1) r1 = RTSP $ \ev2 -> + if ev2 `isBefore` ev1 -- hpc says this is always true. Can this be proved?+ then sAccum evs $ runRTSP r1 ev2+ else Emit ev1 (sAccum evs1 $ runRTSP r ev2) (rAccum evs r1)+ rAccum [] r1 = RTSP $ \ev -> sAccum [] $ runRTSP r1 ev + sAccum evs1@(ev1:evs1a) es2@(Emit ev2 es2a r2a) =+ let+ future = + if ev2 `isBefore` ev1+ then Emit ev2 (sAccum evs1 es2a) (rAccum (esPeek future) r2a)+ else Emit ev1 (sAccum evs1a es2) (rAccum (esPeek future) r2a)+ in future+ sAccum [] es2@(Emit ev2 es2a r2) = Emit ev2 (sAccum [] es2a) (rAccum (esPeek es2) r2)+ sAccum evs1@(ev1:evs1a) k2@(Wait r2) = Emit ev1 (sAccum evs1a k2) (rAccum evs1 r2)+ sAccum [] (Wait r2) = Wait $ accumulate r2+ sAccum evs Finish = streamFromList evs+ ++-- | Repeat each input event after the specified delays until a new event arrives, at which point the sequence begins again+-- with the new event value. The list of delays must not be negative and must be in ascending order. All the delays are+-- relative to the first event.+--+-- Be careful when using list comprehensions to create the argument. A list like +--+-- > [1..5] :: NominalDiffTime+--+-- will count up in picoseconds rather than seconds, which is probably not what is wanted. Instead use+--+-- > map fromInteger [1..5] :: NominalDiffTime+repeatEvent :: [NominalDiffTime] -> RTSP b b+repeatEvent dts1 = RTSP $ \(Event t0 v) ->+ let+ rStream dt0 (dt:dts2) + | dt0 <= dt = Emit (Event (dt `addUTCTime` t0) v) (rStream dt dts2) (repeatEvent dts1)+ | otherwise = error "Control.Applicative.RTSP.streamRepeat: negative time increment."+ rStream _ [] = Wait (repeatEvent dts1)+ in rStream 0 dts1+++-- | Delay input events by the specified time, but given an event stream @{ev1, ev2, ev3...}@, if ev2 arrives before+-- ev1 has been emitted then ev1 will be lost.+delay0 :: NominalDiffTime -> RTSP b b+delay0 dt = repeatEvent [dt]+++-- | Delay input events by the specified time.+-- +-- Unfortunately this requires O(n) time when there are @n@ events queued up due to the use of "accumulate".+delay :: NominalDiffTime -> RTSP b b+delay = accumulate . delay0+++-- | A conditional stream: events matching the predicate will be passed to the stream.+type Cond a b = (a -> Bool, RTSP a b)+++-- | Conditional stream execution: only certain events will be accepted.+streamFilter :: Cond a b -> RTSP a b+streamFilter (p, r1) = rtspFilter p >>> r1 ++++-- | Send each event to all the streams that accept it.+cond :: [Cond a b] -> RTSP a b+cond = mconcat . map streamFilter+++-- | Send each event to the first stream that accepts it, if any.+cond1 :: [Cond a b] -> RTSP a b+cond1 = foldr ifThenElse eventSink+++-- | Send each event to the conditional stream if it accepts it, otherwise send it to the second argument.+--+-- @ifThenElse (p, rThen) rElse@ is equivalent to+--+-- > streamFilter (p, rThen) `mappend` streamFilter (not . p, rElse)+--+-- However @ifThenElse@ only evaluates @p@ once for each input event.+ifThenElse :: Cond a b -> RTSP a b -> RTSP a b+ifThenElse (p,rThen) rElse = ifRTSP (Wait rThen) (Wait rElse)+ where+ ifRTSP es1 es2 = RTSP $ \ev ->+ if p $ eventValue ev+ then ifStream (esProcess ev es1) es2+ else ifStream es1 (esProcess ev es2)+ ifStream Finish Finish = Finish+ ifStream Finish es@(Wait _) = Wait $ ifRTSP Finish es+ ifStream Finish es@(Emit e es1 _) = Emit e (ifStream Finish es1) (ifRTSP Finish es)+ ifStream es@(Wait _) Finish = Wait $ ifRTSP es Finish+ ifStream es@(Emit e es1 _) Finish = Emit e (ifStream es1 Finish) (ifRTSP es Finish) + ifStream es1@(Wait _) es2@(Wait _) =+ Wait $ ifRTSP es1 es2+ ifStream es1@(Emit e es1a _) es2@(Wait _) =+ Emit e (ifStream es1a es2) (ifRTSP es1 es2)+ ifStream es1@(Wait _) es2@(Emit e es2a _) =+ Emit e (ifStream es1 es2a) (ifRTSP es1 es2)+ ifStream es1@(Emit e1 es1a _) es2@(Emit e2 es2a _) =+ if e1 `isBefore` e2+ then Emit e1 (ifStream es1a es2) (ifRTSP es1 es2)+ else Emit e2 (ifStream es1 es2a) (ifRTSP es1 es2)+++-- | Real-time Actions. This monad is used to build sequential processors that can be turned into stream processors.+-- An RTA emits zero or more events in response to each input event, and has a state that persists from one event to the next.+-- In particular, state changes made after a "pause" will be visible to the next event regardless of the relative times.+newtype RTA s c v = RTA {unRTA :: s -> Seq (Event c) -> UTCTime -> (v, s, Seq (Event c), UTCTime)}++{- +In the RTA definition code, the following initial variable letters are used:++ b - The type of input events+ c - The type of output events+ f - A function of whatever type.+ q - Queue of output events, of type Seq (Event c)+ s - Used for both the type and value of the state.+ t - Time, of type UTCTime+ v - Used for both the value returned by the current action and its type.+ z - Non-termination flag. True if the RTSP should respond to future events.+-}++instance Functor (RTA s c) where+ fmap f rv = RTA $ \s q t -> + let (v1, s1, q1, t1) = unRTA rv s q t+ in (f v1, s1, q1, t1)++ +instance Monad (RTA s c) where+ return v = RTA $ \ s q t -> (v, s, q, t)+ rv >>= f = RTA $ \s q t ->+ let (v1, s1, q1, t1) = unRTA rv s q t+ in unRTA (f v1) s1 q1 t1++++-- | Get the current time. This is the event time plus any pauses.+now :: RTA s c UTCTime+now = RTA $ \s q t -> (t, s, q, t)++-- | Get the current state.+get :: RTA s c s+get = RTA $ \s q t -> (s, s, q, t)++-- | Put the current state.+put :: s -> RTA s c ()+put v = RTA $ \_ q t -> ((), v, q, t)++-- | Apply a function to the current state.+modify :: (s -> s) -> RTA s c ()+modify f = fmap f get >>= put++-- | Emit a value as an event.+emit :: c -> RTA s c ()+emit v = RTA $ \s q t -> ((), s, q |> Event t v, t)+++-- | Pause before the next step. This does not actually delay processing; it merely increments the time of any emitted events.+pause :: NominalDiffTime -> RTA s c ()+pause dt+ | dt >= 0 = RTA $ \s q t -> ((), s, q, addUTCTime dt t)+ | otherwise = error $ "pause: negative interval of " ++ show dt+ + + ++-- | Execute an RTA as part of a real time stream processor. +-- +-- When a new event arrives any pending output events will be lost. However any state changes are immediately visible to the +-- next event, even if they occured \"after\" the lost events. For instance, consider this:+--+-- > execRTA 1 $ \_ -> do+-- > n <- get+-- > pause 10+-- > emit n+-- > put (n+1)+-- > return True+-- +-- If this receives events at @t=[0,1,3,20]@ then it will emit @[Event 13 3, Event 30 4]@. The events that would have been emitted+-- at @t=[10,11]@ have been lost, but the state change still occured immediately, regardless of the output schedule.+execRTA :: + s + -- ^ The initial state. State persists between input events.+ -> (b -> RTA s c Bool)+ -- ^ A function from the input value to an action. If the action returns @True@ then subsequent input events+ -- will run the action again. If it returns @False@ then the RTSP finishes and will not respond to further events. + -> RTSP b c+execRTA s f = RTSP $ \ev ->+ let+ t = eventTime ev+ v = eventValue ev+ (z, s1, q, _) = unRTA (f v) s empty t+ queueStream q1 = case viewl q1 of+ EmptyL -> if z then Wait $ execRTA s1 f else nullStream+ c :< q2 -> Emit c (queueStream q2) (if z then execRTA s1 f else eventSink)+ in queueStream q++ +-- | Like "execRTA", except that output events are accumulated.+accumulateRTA :: s -> (b -> RTA s c Bool) -> RTSP b c+accumulateRTA s f = accumulate $ execRTA s f+++
+ test/ArbTest.hs view
@@ -0,0 +1,340 @@+{-# OPTIONS_GHC -fno-warn-orphans -XFlexibleInstances #-}+++module ArbTest (+ ArbEvents (..),+ RtspTest (..),+ interpret,+ compile,+ prop_emitsBefore,+ prop_RTSP,+ prop_rtspMonoid1,+ prop_rtspMonoid2,+ prop_rtspMonoidCommutes,+ prop_rtspMonoidAssociates,+ prop_rtspCategoryId1,+ prop_rtspCategoryId2,+ prop_rtspCategoryAssociates,+ prop_rtspCategoryAssociates2,+ prop_rtspIfThenElse,+ prop_eventCount,+ prop_eventLatch+) where+++-- import Control.Applicative+import Control.RTSP+import Control.Category+import Control.Monad+import Data.Function (on)+import Data.List (groupBy, sortBy, partition)+import Data.Maybe+import Data.Monoid+import Data.Ord+import Data.Time+import Test.QuickCheck+++import Prelude hiding ((.), id)++++-- Orphan instances for RTSP types++instance Arbitrary a => Arbitrary (Event a) where+ arbitrary = do+ dt <- arbitrary+ v <- arbitrary+ return $ Event (dt `addUTCTime` epoch) v+ shrink = shrinkNothing+++instance Arbitrary NominalDiffTime where+ arbitrary = fmap (fromRational . abs) arbitrary+ shrink dt = if dt == dtSecs then [] else [dtSecs]+ where dtSecs = fromInteger $ floor dt ++++-- | Base time for events.+epoch :: UTCTime+epoch = UTCTime (fromGregorian 2000 1 1) 0+++-- | A list of arbitrary events in chronological order+newtype ArbEvents a = ArbEvents [Event a]++instance (Show a) => Show (ArbEvents a) where+ show (ArbEvents evs) = "[\n" ++ concatMap showEvent evs ++ "]"+ where showEvent (Event t v) = " Event " ++ show (diffUTCTime t epoch) ++ " " ++ show v ++ "\n"++instance (Arbitrary a) => Arbitrary (ArbEvents a) where+ arbitrary = do+ times <- fmap (scanl (flip addUTCTime) epoch) arbitrary+ events <- forM times $ \t -> fmap (Event t) arbitrary+ return $ ArbEvents events+ shrink (ArbEvents evs) = map ArbEvents $ shrink evs+++-- | An RTSP test consists of a descriptive string, a list function and an RTSP. The list function+-- has the same effect on the list of events that the RTSP has on a stream.+data RtspTest a =+ Id+ | Delay Rational+ | Dup Rational+ | Func (a -> a) String+ | If (a -> Bool) String (RtspTest a) (RtspTest a)+ | Pipe (RtspTest a) (RtspTest a)+ | Par (RtspTest a) (RtspTest a)++instance Show (RtspTest a) where+ show Id = "id"+ show (Delay t) = "delay " ++ show t+ show (Dup t) = "duplicate " ++ show t+ show (Func _ str) = "stream " ++ str+ show (If _ str r1 r2) = "(ifThenElse (" ++ str ++ ", " ++ show r1 ++ ") (" ++ show r2 ++ "))"+ show (Pipe r1 r2) = "(" ++ show r1 ++ " >>> " ++ show r2 ++ ")"+ show (Par r1 r2) = "(" ++ show r1 ++ " `mappend` " ++ show r2 ++ ")"+++instance (Integral a) => Arbitrary (RtspTest a) where+ arbitrary = do+ frequency [+ (2, return Id),+ (1, fmap (Delay . fromRational . abs) arbitrary),+ (1, fmap (Dup . fromRational . abs) arbitrary),+ (1, oneof [return $ Func (*2) "(*2)",+ return $ Func (+1) "(+1)",+ return $ Func (*3) "(*3)"]),+ (1, do+ (p, str) <- elements [(odd, "odd"),+ (even, "even"),+ ((== 0) . (`mod` 3), "mult3")]+ return (If p str) `ap` arbitrary `ap` arbitrary),+ (1, return Pipe `ap` arbitrary `ap` arbitrary),+ (1, return Par `ap` arbitrary `ap` arbitrary)+ ] + shrink Id = []+ shrink (Delay dt) = Id : map Delay (shrink dt)+ shrink (Dup dt) = Delay dt : map Dup (shrink dt)+ shrink (Func _ _) = [Id]+ shrink (If p str r1 r2) = shrinkBinaryOp (If p str) r1 r2+ shrink (Pipe r1 r2) = shrinkBinaryOp Pipe r1 r2+ shrink (Par r1 r2) = shrinkBinaryOp Par r1 r2+++shrinkBinaryOp :: (Integral a) => (RtspTest a -> RtspTest a -> RtspTest a) -> RtspTest a -> RtspTest a -> [RtspTest a] +shrinkBinaryOp op r1 r2 = concat [+ [Id, r1, r2], + map (\r -> op r r2) $ shrink r1,+ map (op r1) $ shrink r2]+++-- | Interpret a test on a list, predicting the output for the equivalent arrow.+interpret :: (Num a) => RtspTest a -> [Event a] -> [Event a]+interpret Id evs = evs+interpret (Delay dt) evs = map (\(Event t v) -> Event (addUTCTime (fromRational dt) t) v) evs+interpret (Dup dt) evs =+ foldl merge [] $ map (\(Event t v) -> [Event t v, Event (addUTCTime (fromRational dt) t) v]) evs+interpret (Func f _) evs = map (fmap f) evs+interpret (If p _ r1 r2) evs = merge (interpret r1 thens) (interpret r2 elses)+ where + (thens, elses) = partition (p . eventValue) evs+interpret (Pipe r1 r2) evs = interpret r2 $ interpret r1 evs+interpret (Par r1 r2) evs = merge (interpret r1 evs) (interpret r2 evs)++++-- | Merge two sorted lists of events.+merge :: [Event a] -> [Event a] -> [Event a]+merge xs [] = xs+merge [] ys = ys+merge xs@(x:xs1) ys@(y:ys1) = if y `isBefore` x then y : merge ys1 xs else x : merge ys xs1++++-- | Compile a test into an arrow.+compile :: (Num a) => RtspTest a -> RTSP a a+compile Id = id+compile (Delay dt) = delay (fromRational dt)+compile (Dup dt) = accumulate $ repeatEvent [0, fromRational dt]+compile (Func f _) = stream f+compile (If p _ r1 r2) = ifThenElse (p, compile r1) (compile r2)+compile (Pipe r1 r2) = compile r1 >>> compile r2+compile (Par r1 r2) = compile r1 `mappend` compile r2+++-- | Two event streams are equivalent regardless of the ordering of simultaneous events+isEquivalent :: (Ord a) => [Event a] -> [Event a] -> Bool+isEquivalent xs ys = normalise xs == normalise ys+ where normalise = map (sortBy (comparing eventValue)) . groupBy ((==) `on` eventTime)+++-- | Assert that "compile" and "interpret" are equivalent.+prop_RTSP :: RtspTest Integer -> ArbEvents Integer -> Property+prop_RTSP tst (ArbEvents evs) = printTestCase failStr $ result1 `isEquivalent` result2+ where+ result1 = interpret tst evs+ result2 = simulateRTSP (compile tst) evs+ failStr = concat [ "interpret => ", show (ArbEvents result1), "\n",+ "compile => ", show (ArbEvents result2), "\n"]+ ++++-- | Reify primitive RTA actions.+data RtaTest s c =+ Modify (s -> s) | Emit (s -> c) | Pause (s -> NominalDiffTime)+++instance (Arbitrary s, CoArbitrary s, Arbitrary c) => Arbitrary (RtaTest s c)+ where+ arbitrary = frequency [+ (3, return Modify `ap` arbitrary),+ (1, return Emit `ap` arbitrary),+ (5, return Pause `ap` arbitrary)+ ]+ shrink = shrinkNothing++instance Show (RtaTest s c) where+ show (Modify _) = "Modify"+ show (Emit _) = "Emit"+ show (Pause _) = "Pause"++++instance (Integral s, Arbitrary s, CoArbitrary s, Arbitrary c) => Arbitrary (RTA s c Bool) where+ arbitrary = fmap execRtaTests arbitrary+ shrink = shrinkNothing++instance (CoArbitrary b, Arbitrary c) => Arbitrary (RTSP b c) where+ arbitrary = do+ rtaF <- arbitrary+ return $ execRTA (0 :: Integer) rtaF+ shrink = shrinkNothing+++-- | Execute an RtaTest +execRtaTest :: RtaTest s c -> RTA s c ()+execRtaTest (Modify f) = fmap f get >>= put+execRtaTest (Emit f) = fmap f get >>= emit+execRtaTest (Pause f) = fmap f get >>= pause+++-- | Execute a sequence of RtaTests as a single action.+execRtaTests :: (Integral s) => [RtaTest s c] -> RTA s c Bool+execRtaTests ts = do+ mapM_ execRtaTest ts+ s <- get+ -- return True+ return $ (s `mod` 20) /= 0+++type RtspProp = ArbEvents Integer -> Property++type RII = RTSP Integer Integer++rtspEquivalent :: (CoArbitrary b, Arbitrary c, Ord c, Show c) => RTSP b c -> RTSP b c -> ArbEvents b -> Property+rtspEquivalent r1 r2 (ArbEvents evs) = printTestCase failStr $ trace r1 `isEquivalent` trace r2+ where+ trace r = simulateRTSP r evs+ failStr = concat [+ "Trace1 = ", show $ ArbEvents $ trace r1, "\n",+ "Trace2 = ", show $ ArbEvents $ trace r2, "\n"]+ + +prop_emitsBefore :: RII -> RII -> Event Integer -> Property+prop_emitsBefore r1 r2 ev =+ printTestCase (show (evs1, evs2)) $+ case (evs1, evs2) of+ (Nothing, Nothing) -> not (es1 `emitsBefore` es2 || es2 `emitsBefore` es1)+ (Just _, Nothing) -> es1 `emitsBefore` es2 && not (es2 `emitsBefore` es1)+ (Nothing, Just _ ) -> not (es1 `emitsBefore` es2) && es2 `emitsBefore` es1+ (Just e1, Just e2) -> (e1 `isBefore` e2) == (es1 `emitsBefore` es2) &&+ (e2 `isBefore` e1) == (es2 `emitsBefore` es1)+ where+ es1 = runRTSP r1 ev+ es2 = runRTSP r2 ev+ evs1 = listToMaybe $ esPeek es1+ evs2 = listToMaybe $ esPeek es2++prop_rtspMonoid1 :: RII -> RtspProp +prop_rtspMonoid1 r = rtspEquivalent r (mempty `mappend` r)++prop_rtspMonoid2 :: RII -> RtspProp+prop_rtspMonoid2 r = rtspEquivalent r (r `mappend` mempty)+++prop_rtspMonoidCommutes :: RII -> RII -> RtspProp+prop_rtspMonoidCommutes r1 r2 = rtspEquivalent (r1 `mappend` r2) (r2 `mappend` r1)++prop_rtspMonoidAssociates :: RII -> RII -> RII -> RtspProp+prop_rtspMonoidAssociates r1 r2 r3 = rtspEquivalent (r1 `mappend` (r2 `mappend` r3)) ((r1 `mappend` r2) `mappend` r3)+++prop_rtspCategoryId1 :: RII -> RtspProp+prop_rtspCategoryId1 r = rtspEquivalent r (id >>> r)++prop_rtspCategoryId2 :: RII -> RtspProp+prop_rtspCategoryId2 r = rtspEquivalent r (r >>> id)+++prop_rtspCategoryAssociates :: RII -> RII -> RII -> RtspProp+prop_rtspCategoryAssociates r1 r2 r3 = rtspEquivalent (r1 >>> (r2 >>> r3)) ((r1 >>> r2) >>> r3)++prop_rtspCategoryAssociates2 :: RtspTest Integer -> RtspTest Integer -> RtspTest Integer -> RtspProp+prop_rtspCategoryAssociates2 rt1 rt2 rt3 = rtspEquivalent (r1 >>> (r2 >>> r3)) ((r1 >>> r2) >>> r3)+ where+ r1 = compile rt1+ r2 = compile rt2+ r3 = compile rt3+++-- | @ifThenElse (p, rThen) rElse@ is equivalent to+--+-- > streamFilter (p, rThen) `mappend` streamFilter (not . p, rElse)+prop_rtspIfThenElse :: RII -> RII -> RtspProp+prop_rtspIfThenElse r1 r2 = rtspEquivalent+ (ifThenElse (odd, r1) r2)+ (streamFilter (odd, r1) `mappend` streamFilter (not . odd, r2))+-- where+-- r1 = compile rt1+-- r2 = compile rt2+ ++traceEquivalent :: (Ord a, Show a) => [Event a] -> [Event a] -> Property+traceEquivalent trace1 trace2 = + printTestCase failStr $ trace1 `isEquivalent` trace2+ where failStr = concat [+ "Trace 1 = ", show $ ArbEvents trace1, "\n",+ "Trace 2 = ", show $ ArbEvents trace2, "\n"]+ + +-- | Count events. +eventCount :: RTSP b (Integer, b)+eventCount = execRTA 0 $ \v -> do+ s <- fmap (+1) get+ put s+ emit (s, v)+ return True++prop_eventCount :: RtspProp+prop_eventCount (ArbEvents evs) = traceEquivalent+ (simulateRTSP eventCount evs) + (zipWith (\n -> fmap (\v -> (n, v))) [1..] evs)++++-- | Repeat each event value once a second ten times.+eventLatch :: RTSP b b+eventLatch = accumulateRTA () $ \v -> do+ replicateM_ 5 (emit v >> pause 1)+ return True+ +prop_eventLatch :: RtspProp+prop_eventLatch (ArbEvents evs) = traceEquivalent+ (simulateRTSP eventLatch evs)+ (sortBy (comparing eventTime) $ concatMap rep evs)+ where+ rep (Event t v) = [Event (n `addUTCTime` t) v | n <- [0,1..4]]+
+ test/Main.hs view
@@ -0,0 +1,40 @@++module Main where++import ArbTest+import Test.Framework+import Test.Framework.Providers.QuickCheck2 (testProperty)++longTest :: TestOptions+longTest = TestOptions Nothing (Just 1000) Nothing Nothing Nothing++main :: IO ()+main = defaultMain tests++tests :: [Test]+tests = map (longTest `plusTestOptions`)+ [testGroup "Arbitrary RTSP list equivalence"+ [testProperty "compile == interpret" prop_RTSP,+ testProperty "ifThenElse definition" prop_rtspIfThenElse+ ],+ testGroup "Event order consistency"+ [testProperty "emitsBefore consistent with isBefore" prop_emitsBefore+ ],+ testGroup "Monoid laws, plus commutivity"+ [testProperty "Monoid left identity" prop_rtspMonoid1,+ testProperty "Monoid right identity" prop_rtspMonoid2,+ testProperty "Monoid commutes" prop_rtspMonoidCommutes,+ testProperty "Monoid associates" prop_rtspMonoidAssociates+ ],+ testGroup "Category laws"+ [testProperty "Category left identity" prop_rtspCategoryId1,+ testProperty "Category right identity" prop_rtspCategoryId2,+ testProperty "Category associates" prop_rtspCategoryAssociates,+ testProperty "Category associates 2" prop_rtspCategoryAssociates2+ ],+ testGroup "Sample RTAs"+ [+ testProperty "Event Count" prop_eventCount,+ testProperty "Event Latch" prop_eventLatch+ ] + ]