elerea 0.3.0 → 0.4.0
raw patch · 5 files changed
+144/−79 lines, 5 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- FRP.Elerea.Internal: Sample :: a -> SignalTrans a
+ FRP.Elerea: (.@.) :: Signal a -> Signal t -> Signal a
+ FRP.Elerea: keepAlive :: Signal a -> Signal t -> Signal a
+ FRP.Elerea.Internal: SND :: a -> (Signal a) -> SignalNode a
+ FRP.Elerea.Internal: SNKA :: (Signal a) -> (Signal t) -> SignalNode a
+ FRP.Elerea.Internal: Sampled :: a -> (SignalNode a) -> SignalTrans a
+ FRP.Elerea.Internal: age :: Signal a -> DTime -> IO ()
+ FRP.Elerea.Internal: delay :: a -> Signal a -> Signal a
+ FRP.Elerea.Internal: keepAlive :: Signal a -> Signal t -> Signal a
Files
- CHANGES +5/−0
- FRP/Elerea.hs +4/−6
- FRP/Elerea/Graph.hs +10/−0
- FRP/Elerea/Internal.hs +124/−72
- elerea.cabal +1/−1
CHANGES view
@@ -1,3 +1,8 @@+0.4.0 - 0904??+* added keepAlive+* made delay a primitive+* completely separated sampling and aging+ 0.3.0 - 090419 * documentation bug fixed: the latcher is not delayed * added dot (Graphviz) converter
FRP/Elerea.hs view
@@ -33,7 +33,7 @@ Time, DTime, Sink, Signal,- superstep,+ superstep, keepAlive, (.@.), stateful, transfer, latcher, external, delay, edge, (==@), (/=@), (<@), (<=@), (>=@), (>@),@@ -47,12 +47,10 @@ infixr 3 &&@ infixr 2 ||@ -{-| The `delay` transfer function emits the value of a signal from the-previous superstep, starting with the filler value given in the first-argument. -}+{-| A short alternative name for 'keepAlive'. -} -delay :: a -> Signal a -> Signal a-delay v0 s = snd <$> transfer (v0,v0) (\_ v' (v,_) -> (v',v)) s+(.@.) :: Signal a -> Signal t -> Signal a+(.@.) = keepAlive {-| The `edge` transfer function takes a bool signal and emits another bool signal that turns true only at the moment when there is a rising
FRP/Elerea/Graph.hs view
@@ -28,6 +28,7 @@ | App Id Id | Latcher Id Id Id | External+ | Delay Id | Lift1 Id | Lift2 Id Id | Lift3 Id Id Id@@ -63,6 +64,12 @@ (ss',st''') <- buildStore st'' ss return (Map.insert p (Latcher s' e' ss') st''') insertSignal st p (SNR _) = return (Map.insert p External st)+insertSignal st p (SND _ s) = do+ (s',st') <- buildStore (Map.insert p None st) s+ return (Map.insert p (Delay s') st')+insertSignal st p (SNKA (S r) _) = do+ Ready s <- readIORef r+ insertSignal st p s insertSignal st p (SNL1 _ s1) = do (s1',st') <- buildStore (Map.insert p None st) s1 return (Map.insert p (Lift1 s1') st')@@ -97,6 +104,7 @@ App _ _ -> "app" Latcher _ _ _ -> "latcher" External -> "external"+ Delay _ -> "delay" Lift1 _ -> "fun1" Lift2 _ _ -> "fun2" Lift3 _ _ _ -> "fun3"@@ -145,10 +153,12 @@ Latcher _ _ _ -> "99ccff" External -> "ccff99" Stateful -> "ffffcc"+ Delay _ -> "ffccff" _ -> "ffffff" nodeShape = case n of Transfer _ -> "diamond" Latcher _ _ _ -> "hexagon" External -> "invtriangle"+ Delay _ -> "box" _ -> "ellipse" return $ "digraph G {\n" ++ concat rules ++ "}\n"
FRP/Elerea/Internal.hs view
@@ -24,21 +24,25 @@ and mutates all the variables the signal depends on. It is supposed to be called repeatedly in a loop that also takes care of user input. -To ensure consistency, a superstep has two phases: evaluation and-finalisation. During evaluation, each signal affected is sampled at-the current point of time ('sample'), advanced by the desired time-('advance'), and both of these pieces of data are stored in its+To ensure consistency, a superstep has three phases: sampling, aging+and finalisation. Each signal reachable from the top-level signal+passed to 'superstep' is sampled at the current point of time+('sample'), and the sample is stored along with the old signal in its reference. If the value of a signal is requested multiple times, the-sample is simply reused, and no further aging is performed. After-successfully sampling the top-level signal, the finalisation process+sample is simply reused. After successfully sampling the top-level+signal, the network is traversed again to advance by the desired time+('advance'), and when that's completed, the finalisation process throws away the intermediate samples and marks the aged signals as the current ones, ready to be sampled again. If there is a dependency-loop, the system tries to use the `sampleDelayed` function instead of-`sample` to get a useful value at the problematic spot instead of-entering an infinite loop. Evaluation is done by the 'signalValue'-function, while finalisation is done by 'commit'. Since these-functions are invoked recursively on a data structure with existential-types, their types also need to be explicity quantified.+loop, the system tries to use the 'sampleDelayed' function instead of+'sample' to get a useful value at the problematic spot instead of+entering an infinite loop. Evaluation is initiated by the+'signalValue' function (which is used in both the sampling and the+aging phase to calculate samples and retrieve the cached values if+they are requested again), aging is performed by 'age', while+finalisation is done by 'commit'. Since these functions are invoked+recursively on a data structure with existential types, their types+also need to be explicity quantified. As a bonus, applicative nodes are automatically collapsed into lifted functions of up to five arguments. This optimisation significantly@@ -82,12 +86,10 @@ -- | @Sampling s@ is still @s@ after its current value was -- requested, but still not delivered | Sampling (SignalNode a)- -- | @Sample x@ is just the value @x@, eventually to be replaced- -- by the aged version of its corresponding signal- | Sample a- -- | @Aged x s@ is an already sampled signal, where @x@ is the- -- current value and @s@ is the new version of the signal for the- -- next superstep+ -- | @Sampled x s@ is signal @s@ paired with its current value @x@+ | Sampled a (SignalNode a)+ -- | @Aged x s@ is the aged version of signal @s@ paired with its+ -- current value @x@ | Aged a (SignalNode a) {-| The possible structures of a node are defined by the 'SignalNode'@@ -111,6 +113,10 @@ | SNE (Signal a) (Signal Bool) (Signal (Signal a)) -- | @SNR r@: opaque reference to connect peripherals | SNR (IORef a)+ -- | @SND s@: the @s@ signal delayed by one superstep+ | SND a (Signal a)+ -- | @SNKA s l@: equivalent to @s@ while aging signal @l@+ | forall t . SNKA (Signal a) (Signal t) -- | @SNL1 f@: @fmap f@ | forall t . SNL1 (t -> a) (Signal t) -- | @SNL2 f@: @liftA2 f@@@ -252,66 +258,86 @@ createSignal :: SignalNode a -> Signal a createSignal = S . unsafePerformIO . newIORef . Ready -{-| Sampling and aging the signal and all of its dependencies, at the-same time. We don't need the aged signal in the current superstep,-only the current value, so we sample before propagating the changes,-which might require the fresh sample because of recursive-definitions. -}+{-| Sampling the signal and all of its dependencies, at the same time.+We don't need the aged signal in the current superstep, only the+current value, so we sample before propagating the changes, which+might require the fresh sample because of recursive definitions. -} signalValue :: forall a . Signal a -> DTime -> IO a signalValue (S r) dt = do t <- readIORef r case t of- Ready s -> do writeIORef r (Sampling s)- -- TODO: advance can be evaluated in a separate- -- thread, since we don't need its result right- -- away, only in the next superstep.- v <- sample s dt- -- We memorise the sample to handle loops- -- nicely. The undefined future signal cannot- -- bite us, because we don't need it during the- -- evaluation phase.- writeIORef r (Sample v)- s' <- advance s v dt- writeIORef r (Aged v s')- return v- Sampling s -> do -- We started sampling this already, so there is- -- a dependency cycle we have to resolve by- -- adding a delay to stateful signals. Stateless- -- signals should not form a loop, which is- -- obvious...- v <- sampleDelayed s dt- writeIORef r (Sample v)- -- Since we are sampling this already, aging- -- will be performed by the case above. Also,- -- the result is memoised by the system, so we- -- are not calculating anything twice. Note- -- that this is an old value, so it shouldn't be- -- used for aging anyway.- return v- Sample v -> return v- Aged v _ -> return v+ Ready s -> do writeIORef r (Sampling s)+ -- TODO: advance can be evaluated in a separate+ -- thread, since we don't need its result right+ -- away, only in the next superstep.+ v <- sample s dt+ -- We memorise the sample to handle loops+ -- nicely. The undefined future signal cannot+ -- bite us, because we don't need it during the+ -- evaluation phase.+ writeIORef r (Sampled v s)+ return v+ Sampling s -> do -- We started sampling this already, so there is+ -- a dependency cycle we have to resolve by+ -- adding a delay to stateful signals. Stateless+ -- signals should not form a loop, which is+ -- obvious...+ v <- sampleDelayed s dt+ writeIORef r (Sampled v s)+ -- Since we are sampling it already, this node+ -- will be overwritten by the case above when+ -- the loop is closed.+ return v+ Sampled v _ -> return v+ Aged v _ -> return v -{-| Finalising the aged signals for the next round. -}+{-| Aging the network of signals the given signal depends on. -} +age :: forall a . Signal a -> DTime -> IO ()+age (S r) dt = do+ t <- readIORef r+ case t of+ Sampled v s -> do s' <- advance s v dt+ writeIORef r (Aged v s')+ -- TODO: branching can be trivially parallelised+ case s' of+ SNT s _ _ -> age s dt+ SNA sf sx -> age sf dt >> age sx dt+ SNE s e ss -> age s dt >> age e dt >> age ss dt+ SND _ s -> age s dt + SNKA s l -> age s dt >> age l dt+ SNL1 _ s -> age s dt + SNL2 _ s1 s2 -> age s1 dt >> age s2 dt+ SNL3 _ s1 s2 s3 -> age s1 dt >> age s2 dt >> age s3 dt+ SNL4 _ s1 s2 s3 s4 -> age s1 dt >> age s2 dt >> age s3 dt >> age s4 dt+ SNL5 _ s1 s2 s3 s4 s5 -> age s1 dt >> age s2 dt >> age s3 dt >> age s4 dt >> age s5 dt+ _ -> return ()+ Aged _ _ -> return () + _ -> error "Inconsistent state: signal not sampled properly!"++{-| Finalising aged signals for the next round. -}+ commit :: forall a . Signal a -> IO ()-commit (S s) = do- t <- readIORef s+commit (S r) = do+ t <- readIORef r case t of- Aged _ s' -> do writeIORef s (Ready s')- -- TODO: branching can be trivially parallelised- case s' of- SNT s _ _ -> commit s- SNA sf sx -> commit sf >> commit sx- SNL1 _ s -> commit s- SNL2 _ s1 s2 -> commit s1 >> commit s2- SNL3 _ s1 s2 s3 -> commit s1 >> commit s2 >> commit s3- SNL4 _ s1 s2 s3 s4 -> commit s1 >> commit s2 >> commit s3 >> commit s4- SNL5 _ s1 s2 s3 s4 s5 -> commit s1 >> commit s2 >> commit s3 >> commit s4 >> commit s5- SNE s e ss -> commit s >> commit e >> commit ss- _ -> return ()- Ready _ -> return () - _ -> error "Inconsistent state: signal not aged!"+ Aged _ s -> do writeIORef r (Ready s)+ -- TODO: branching can be trivially parallelised+ case s of+ SNT s _ _ -> commit s+ SNA sf sx -> commit sf >> commit sx+ SNE s e ss -> commit s >> commit e >> commit ss+ SND _ s -> commit s + SNKA s l -> commit s >> commit l+ SNL1 _ s -> commit s + SNL2 _ s1 s2 -> commit s1 >> commit s2+ SNL3 _ s1 s2 s3 -> commit s1 >> commit s2 >> commit s3+ SNL4 _ s1 s2 s3 s4 -> commit s1 >> commit s2 >> commit s3 >> commit s4+ SNL5 _ s1 s2 s3 s4 s5 -> commit s1 >> commit s2 >> commit s3 >> commit s4 >> commit s5+ _ -> return ()+ Ready _ -> return ()+ _ -> error "Inconsistent state: signal not aged properly!" {-| Aging the signal. Stateful signals have their state forced to prevent building up big thunks, and the latcher also does its job@@ -326,6 +352,8 @@ if b then return (SNE s' e ss) else return sw+advance (SND _ s) _ dt = do x <- signalValue s dt+ return (SND x s) advance s _ _ = return s {-| Sampling the signal at the current moment. This is where static@@ -343,6 +371,9 @@ s' <- signalValue ss dt signalValue (if b then s' else s) dt sample (SNR r) _ = readIORef r+sample (SND v _) _ = return v+sample (SNKA s l) dt = do signalValue l dt+ signalValue s dt sample (SNL1 f s) dt = f <$> signalValue s dt sample (SNL2 f s1 s2) dt = liftM2 f (signalValue s1 dt) (signalValue s2 dt) sample (SNL3 f s1 s2 s3) dt = liftM3 f (signalValue s1 dt) (signalValue s2 dt) (signalValue s3 dt)@@ -372,6 +403,7 @@ -> IO a -- ^ the current value of the signal superstep world dt = do snapshot <- signalValue world dt+ age world dt commit world return snapshot @@ -394,8 +426,8 @@ transfer x0 f s = createSignal (SNT s x0 f) {-| Reactive signal that starts out as @s@ and can change its-behaviour to the one supplied in @ss@ whenever @e@ is true. The change-can be observed immediately, unless the signal is sampled by+behaviour to the one supplied in @ss@ whenever @e@ is true. The+change can be observed immediately, unless the signal is sampled by `sampleDelayed`, which puts a delay on the latch control (but not on the latched signal!). -} @@ -406,7 +438,7 @@ latcher s e ss = createSignal (SNE s e ss) {-| A signal that can be directly fed through the sink function-returned. This can be used to attach the network to the outer+returned. This can be used to attach the network to the outer world. -} external :: a -- ^ initial value@@ -415,3 +447,23 @@ ref <- newIORef x0 snr <- newIORef (Ready (SNR ref)) return (S snr,writeIORef ref)++{-| The `delay` transfer function emits the value of a signal from the+previous superstep, starting with the filler value given in the first+argument. It has to be a primitive, otherwise it could not be used to+prevent automatic delays. -}++delay :: a -- ^ initial output+ -> Signal a -- ^ the signal to delay+ -> Signal a+delay x0 s = createSignal (SND x0 s)++{-| Dependency injection to allow aging signals whose output is not+necessarily needed to produce the current sample of the first+argument. It is more or less equivalent to @liftA2 const@, the+difference being that it evaluates its second argument first. -}++keepAlive :: Signal a -- ^ the actual output+ -> Signal t -- ^ a signal guaranteed to age when this one is sampled+ -> Signal a+keepAlive s l = createSignal (SNKA s l)
elerea.cabal view
@@ -1,5 +1,5 @@ Name: elerea-Version: 0.3.0+Version: 0.4.0 Cabal-Version: >= 1.2 Synopsis: A minimalistic FRP library Category: reactivity, FRP