elerea 2.8.0 → 2.9.0
raw patch · 9 files changed
+1948/−2708 lines, 9 filessetup-changedPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
- FRP.Elerea.Clocked: data Signal a
- FRP.Elerea.Clocked: data SignalGen a
- FRP.Elerea.Clocked: delay :: a -> Signal a -> SignalGen (Signal a)
- FRP.Elerea.Clocked: effectful :: IO a -> SignalGen (Signal a)
- FRP.Elerea.Clocked: effectful1 :: (t -> IO a) -> Signal t -> SignalGen (Signal a)
- FRP.Elerea.Clocked: effectful2 :: (t1 -> t2 -> IO a) -> Signal t1 -> Signal t2 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: effectful3 :: (t1 -> t2 -> t3 -> IO a) -> Signal t1 -> Signal t2 -> Signal t3 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -> Signal t1 -> Signal t2 -> Signal t3 -> Signal t4 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: execute :: IO a -> SignalGen a
- FRP.Elerea.Clocked: external :: a -> IO (Signal a, a -> IO ())
- FRP.Elerea.Clocked: externalMulti :: IO (SignalGen (Signal [a]), a -> IO ())
- FRP.Elerea.Clocked: generator :: Signal (SignalGen a) -> SignalGen (Signal a)
- FRP.Elerea.Clocked: global :: SignalGen a -> SignalGen a
- FRP.Elerea.Clocked: instance Applicative Signal
- FRP.Elerea.Clocked: instance Applicative SignalGen
- FRP.Elerea.Clocked: instance Bounded t => Bounded (Signal t)
- FRP.Elerea.Clocked: instance Enum t => Enum (Signal t)
- FRP.Elerea.Clocked: instance Eq (Signal a)
- FRP.Elerea.Clocked: instance Floating t => Floating (Signal t)
- FRP.Elerea.Clocked: instance Fractional t => Fractional (Signal t)
- FRP.Elerea.Clocked: instance Functor Signal
- FRP.Elerea.Clocked: instance Functor SignalGen
- FRP.Elerea.Clocked: instance Integral t => Integral (Signal t)
- FRP.Elerea.Clocked: instance Monad Signal
- FRP.Elerea.Clocked: instance Monad SignalGen
- FRP.Elerea.Clocked: instance MonadBase SignalGen SignalGen
- FRP.Elerea.Clocked: instance MonadFix SignalGen
- FRP.Elerea.Clocked: instance MonadIO SignalGen
- FRP.Elerea.Clocked: instance Num t => Num (Signal t)
- FRP.Elerea.Clocked: instance Ord t => Ord (Signal t)
- FRP.Elerea.Clocked: instance Real t => Real (Signal t)
- FRP.Elerea.Clocked: instance Show (Signal a)
- FRP.Elerea.Clocked: memo :: Signal a -> SignalGen (Signal a)
- FRP.Elerea.Clocked: snapshot :: Signal a -> SignalGen a
- FRP.Elerea.Clocked: start :: SignalGen (Signal a) -> IO (IO a)
- FRP.Elerea.Clocked: stateful :: a -> (a -> a) -> SignalGen (Signal a)
- FRP.Elerea.Clocked: transfer :: a -> (t -> a -> a) -> Signal t -> SignalGen (Signal a)
- FRP.Elerea.Clocked: transfer2 :: a -> (t1 -> t2 -> a -> a) -> Signal t1 -> Signal t2 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: transfer3 :: a -> (t1 -> t2 -> t3 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: transfer4 :: a -> (t1 -> t2 -> t3 -> t4 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> Signal t4 -> SignalGen (Signal a)
- FRP.Elerea.Clocked: until :: Signal Bool -> SignalGen (Signal Bool)
- FRP.Elerea.Clocked: withClock :: Signal Bool -> SignalGen a -> SignalGen a
- FRP.Elerea.Param: instance Applicative (SignalGen p)
- FRP.Elerea.Param: instance Applicative Signal
- FRP.Elerea.Param: instance Bounded t => Bounded (Signal t)
- FRP.Elerea.Param: instance Enum t => Enum (Signal t)
- FRP.Elerea.Param: instance Eq (Signal a)
- FRP.Elerea.Param: instance Floating t => Floating (Signal t)
- FRP.Elerea.Param: instance Fractional t => Fractional (Signal t)
- FRP.Elerea.Param: instance Functor (SignalGen p)
- FRP.Elerea.Param: instance Functor Signal
- FRP.Elerea.Param: instance Integral t => Integral (Signal t)
- FRP.Elerea.Param: instance Monad (SignalGen p)
- FRP.Elerea.Param: instance Monad Signal
- FRP.Elerea.Param: instance MonadBase (SignalGen p) (SignalGen p)
- FRP.Elerea.Param: instance MonadFix (SignalGen p)
- FRP.Elerea.Param: instance MonadIO (SignalGen p)
- FRP.Elerea.Param: instance Num t => Num (Signal t)
- FRP.Elerea.Param: instance Ord t => Ord (Signal t)
- FRP.Elerea.Param: instance Real t => Real (Signal t)
- FRP.Elerea.Param: instance Show (Signal a)
- FRP.Elerea.Param: until :: Signal Bool -> SignalGen p (Signal Bool)
- FRP.Elerea.Simple: instance Applicative Signal
- FRP.Elerea.Simple: instance Applicative SignalGen
- FRP.Elerea.Simple: instance Bounded t => Bounded (Signal t)
- FRP.Elerea.Simple: instance Enum t => Enum (Signal t)
- FRP.Elerea.Simple: instance Eq (Signal a)
- FRP.Elerea.Simple: instance Floating t => Floating (Signal t)
- FRP.Elerea.Simple: instance Fractional t => Fractional (Signal t)
- FRP.Elerea.Simple: instance Functor Signal
- FRP.Elerea.Simple: instance Functor SignalGen
- FRP.Elerea.Simple: instance Integral t => Integral (Signal t)
- FRP.Elerea.Simple: instance Monad Signal
- FRP.Elerea.Simple: instance Monad SignalGen
- FRP.Elerea.Simple: instance MonadBase SignalGen SignalGen
- FRP.Elerea.Simple: instance MonadFix SignalGen
- FRP.Elerea.Simple: instance MonadIO SignalGen
- FRP.Elerea.Simple: instance Num t => Num (Signal t)
- FRP.Elerea.Simple: instance Ord t => Ord (Signal t)
- FRP.Elerea.Simple: instance Real t => Real (Signal t)
- FRP.Elerea.Simple: instance Show (Signal a)
- FRP.Elerea.Simple: until :: Signal Bool -> SignalGen (Signal Bool)
+ FRP.Elerea.Param: instance Control.Monad.Base.MonadBase (FRP.Elerea.Param.SignalGen p) (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance Control.Monad.Fix.MonadFix (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance Control.Monad.IO.Class.MonadIO (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance GHC.Base.Applicative (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance GHC.Base.Applicative FRP.Elerea.Param.Signal
+ FRP.Elerea.Param: instance GHC.Base.Functor (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance GHC.Base.Functor FRP.Elerea.Param.Signal
+ FRP.Elerea.Param: instance GHC.Base.Monad (FRP.Elerea.Param.SignalGen p)
+ FRP.Elerea.Param: instance GHC.Base.Monad FRP.Elerea.Param.Signal
+ FRP.Elerea.Param: instance GHC.Classes.Eq (FRP.Elerea.Param.Signal a)
+ FRP.Elerea.Param: instance GHC.Classes.Ord t => GHC.Classes.Ord (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Enum.Bounded t => GHC.Enum.Bounded (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Enum.Enum t => GHC.Enum.Enum (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Float.Floating t => GHC.Float.Floating (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Num.Num t => GHC.Num.Num (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Real.Fractional t => GHC.Real.Fractional (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Real.Integral t => GHC.Real.Integral (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Real.Real t => GHC.Real.Real (FRP.Elerea.Param.Signal t)
+ FRP.Elerea.Param: instance GHC.Show.Show (FRP.Elerea.Param.Signal a)
+ FRP.Elerea.Param: till :: Signal Bool -> SignalGen p (Signal Bool)
+ FRP.Elerea.Param: unsafeExternal :: a -> IO (Signal a, a -> IO ())
+ FRP.Elerea.Simple: instance Control.Monad.Base.MonadBase FRP.Elerea.Simple.SignalGen FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance Control.Monad.Fix.MonadFix FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance Control.Monad.IO.Class.MonadIO FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance GHC.Base.Applicative FRP.Elerea.Simple.Signal
+ FRP.Elerea.Simple: instance GHC.Base.Applicative FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance GHC.Base.Functor FRP.Elerea.Simple.Signal
+ FRP.Elerea.Simple: instance GHC.Base.Functor FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance GHC.Base.Monad FRP.Elerea.Simple.Signal
+ FRP.Elerea.Simple: instance GHC.Base.Monad FRP.Elerea.Simple.SignalGen
+ FRP.Elerea.Simple: instance GHC.Classes.Eq (FRP.Elerea.Simple.Signal a)
+ FRP.Elerea.Simple: instance GHC.Classes.Ord t => GHC.Classes.Ord (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Enum.Bounded t => GHC.Enum.Bounded (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Enum.Enum t => GHC.Enum.Enum (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Float.Floating t => GHC.Float.Floating (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Num.Num t => GHC.Num.Num (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Real.Fractional t => GHC.Real.Fractional (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Real.Integral t => GHC.Real.Integral (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Real.Real t => GHC.Real.Real (FRP.Elerea.Simple.Signal t)
+ FRP.Elerea.Simple: instance GHC.Show.Show (FRP.Elerea.Simple.Signal a)
+ FRP.Elerea.Simple: till :: Signal Bool -> SignalGen (Signal Bool)
+ FRP.Elerea.Simple: unsafeExternal :: a -> IO (Signal a, a -> IO ())
- FRP.Elerea.Param: external :: a -> IO (Signal a, a -> IO ())
+ FRP.Elerea.Param: external :: a -> IO (SignalGen p (Signal a), a -> IO ())
- FRP.Elerea.Simple: external :: a -> IO (Signal a, a -> IO ())
+ FRP.Elerea.Simple: external :: a -> IO (SignalGen (Signal a), a -> IO ())
Files
- CHANGES +109/−103
- FRP/Elerea.hs +47/−50
- FRP/Elerea/Clocked.hs +0/−822
- FRP/Elerea/Param.hs +752/−733
- FRP/Elerea/Simple.hs +866/−823
- FRP/Elerea/Simple/Pure.hs +85/−85
- LICENSE +28/−28
- Setup.hs +3/−3
- elerea.cabal +58/−61
CHANGES view
@@ -1,103 +1,109 @@-2.8.0 - 140122-* added MonadIO and MonadBase instances for SignalGen (Mitsutoshi Aoe)--2.7.0.2 - 120401-* added some inlining annotations (courtesy of Takano Akio)--2.7.0.1 - 120131-* made externalMulti thread-safe (courtesy of Takano Akio)--2.7.0 - 111223-* fixed an issue with nested signal creations not updating properly- (courtesy of Takano Akio)-* added reference implementation for the simple variant--2.6.0 - 111211-* added snapshot to all variants, which allows sampling signals within- signal generators--2.5.0 - 111122-* added SignalGen liftIO equivalent to assist library writers-* simplified the signatures of effectful* combinators-* updated Param to use the more modern codebase (like Simple and- Clocked); this was necessary to support effectful signals-* temporarily removed the static optimisation from Param-* removed dependency on mersenne-random-* removed legacy branch--2.4.0 - 111111-* added effectful signals to assist library writers--2.3.0 - 110627-* reimplemented clocked variant in a correct and more efficient way--2.2.0 - 110402-* added n-ary transfer functions-* temporarily removed transfer from the clocked variant-* revised documentation--2.1.0 - 100805-* reimplemented the parametric variant in a way that doesn't require- signals to carry the type of the parameter any more-* added the ability to extract the global input in the parametric- variant, and also to override it (input and embed, resp.)-* added until to be able to define switchers that can truly drop- references to old signals-* added debug printing capability to the simple and clocked variants-* made a note about possibly deprecating the delayed variant--2.0.0 - 100718-* moved experimental branch to the top (version 1 went into legacy- status)-* added the clocked variant--1.2.3 - 100131-* added externalMulti to handle events that can fire several times- within a superstep-* added a cache to the noise signal for safety reasons, so it lives in- SignalGen now--1.2.2 - 100115-* added noise signals and the getRandom primitive (using mersenne-random)--1.2.1 - 091204-* modified the &&@ and ||@ operators to short-circuit--1.2.0 - 091202-* added the delayed variant to the experimental branch-* renamed storeJust to (-->) in the experimental branch--1.1.0 - 091126-* added experimental branch with a cleaner semantics--1.0.0 - 090726-* completely renewed interface by introducing the SignalMonad--0.6.0 - 090507-* renamed Void to StartToken-* replaced restarter with the simpler and more versatile startTokens--0.5.0 - 090502-* changed names of internal constructors to match primitives better-* added restarter and (==>)-* removed the unused type synonym Time--0.4.0 - 090501-* 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--0.2.0 - 090412-* removed primitives time and stateless-* removed default delay on stateful combinators and added experimental- cycle detection-* added some non-primitive combinators: delay, edge, comparisons,- logic relations-* added signal instances for various numeric classes--0.1.0 - 090410-* first public version+2.9.0 - 160618 +* removed clocked variant, as it proved to be impractical +* renamed external to unsafeExternal +* introduced safe (managed) external signals +* renamed until to till to avoid name clash with the prelude + +2.8.0 - 140122 +* added MonadIO and MonadBase instances for SignalGen (Mitsutoshi Aoe) + +2.7.0.2 - 120401 +* added some inlining annotations (courtesy of Takano Akio) + +2.7.0.1 - 120131 +* made externalMulti thread-safe (courtesy of Takano Akio) + +2.7.0 - 111223 +* fixed an issue with nested signal creations not updating properly + (courtesy of Takano Akio) +* added reference implementation for the simple variant + +2.6.0 - 111211 +* added snapshot to all variants, which allows sampling signals within + signal generators + +2.5.0 - 111122 +* added SignalGen liftIO equivalent to assist library writers +* simplified the signatures of effectful* combinators +* updated Param to use the more modern codebase (like Simple and + Clocked); this was necessary to support effectful signals +* temporarily removed the static optimisation from Param +* removed dependency on mersenne-random +* removed legacy branch + +2.4.0 - 111111 +* added effectful signals to assist library writers + +2.3.0 - 110627 +* reimplemented clocked variant in a correct and more efficient way + +2.2.0 - 110402 +* added n-ary transfer functions +* temporarily removed transfer from the clocked variant +* revised documentation + +2.1.0 - 100805 +* reimplemented the parametric variant in a way that doesn't require + signals to carry the type of the parameter any more +* added the ability to extract the global input in the parametric + variant, and also to override it (input and embed, resp.) +* added until to be able to define switchers that can truly drop + references to old signals +* added debug printing capability to the simple and clocked variants +* made a note about possibly deprecating the delayed variant + +2.0.0 - 100718 +* moved experimental branch to the top (version 1 went into legacy + status) +* added the clocked variant + +1.2.3 - 100131 +* added externalMulti to handle events that can fire several times + within a superstep +* added a cache to the noise signal for safety reasons, so it lives in + SignalGen now + +1.2.2 - 100115 +* added noise signals and the getRandom primitive (using mersenne-random) + +1.2.1 - 091204 +* modified the &&@ and ||@ operators to short-circuit + +1.2.0 - 091202 +* added the delayed variant to the experimental branch +* renamed storeJust to (-->) in the experimental branch + +1.1.0 - 091126 +* added experimental branch with a cleaner semantics + +1.0.0 - 090726 +* completely renewed interface by introducing the SignalMonad + +0.6.0 - 090507 +* renamed Void to StartToken +* replaced restarter with the simpler and more versatile startTokens + +0.5.0 - 090502 +* changed names of internal constructors to match primitives better +* added restarter and (==>) +* removed the unused type synonym Time + +0.4.0 - 090501 +* 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 + +0.2.0 - 090412 +* removed primitives time and stateless +* removed default delay on stateful combinators and added experimental + cycle detection +* added some non-primitive combinators: delay, edge, comparisons, + logic relations +* added signal instances for various numeric classes + +0.1.0 - 090410 +* first public version
FRP/Elerea.hs view
@@ -1,50 +1,47 @@-{-|--Elerea (Eventless reactivity) is a tiny discrete time FRP-implementation without the notion of event-based switching and-sampling, with first-class signals (time-varying values). Reactivity-is provided through various higher-order constructs that also allow-the user to work with arbitrary time-varying structures containing-live signals. Signals have precise and simple denotational semantics.--Stateful signals can be safely generated at any time through a monadic-interface, while stateless combinators can be used in a purely-applicative style. Elerea signals can be defined recursively, and-external input is trivial to attach. The library comes in three major-variants, one of which you need to import:--* "FRP.Elerea.Simple": signals are plain discrete streams isomorphic-to functions over natural numbers;--* "FRP.Elerea.Param": adds a globally accessible input signal for-convenience;--* "FRP.Elerea.Clocked": adds the ability to freeze whole subnetworks-at will.--Elerea is a minimal library that defines only some basic primitives,-and you are advised to install @elerea-examples@ as well to get an-idea how to build non-trivial systems with it. The examples are-separated in order to minimise the dependencies of the core library.-The @dow@ package contains a full game built on top of the simple-variant.--The basic idea of the implementation is described in the WFLP 2010-paper /Efficient and Compositional Higher-Order Streams/-(<http://sgate.emt.bme.hu/documents/patai/publications/PataiWFLP2010.pdf>).--In short, the basic idea is to define completely dynamic data-flow-networks through a pure combinator-style monadic interface. The-network can be turned into an I/O action that samples it sequentially-by the @start@ function. Under the hood, the network is represented-by mutable variables whose interconnections are encapsulated in-closures, and consistency is ensured by a two-phase update process-(essentially double buffering). The library keeps track of the-variables through weak pointers, so all of the live variables can be-updated (this is necessary to ensure referential transparency), and-unused ones can be garbage collected.---}--module FRP.Elerea where-+{-| + +Elerea (Eventless reactivity) is a tiny discrete time FRP +implementation without the notion of event-based switching and +sampling, with first-class signals (time-varying values). Reactivity +is provided through various higher-order constructs that also allow +the user to work with arbitrary time-varying structures containing +live signals. Signals have precise and simple denotational semantics. + +Stateful signals can be safely generated at any time through a monadic +interface, while stateless combinators can be used in a purely +applicative style. Elerea signals can be defined recursively, and +external input is trivial to attach. The library comes in two major +variants, one of which you need to import: + +* "FRP.Elerea.Simple": signals are plain discrete streams isomorphic +to functions over natural numbers; + +* "FRP.Elerea.Param": adds a globally accessible input signal for +convenience; + +Elerea is a minimal library that defines only some basic primitives, +and you are advised to install @elerea-examples@ as well to get an +idea how to build non-trivial systems with it. The examples are +separated in order to minimise the dependencies of the core library. +The @dow@ package contains a full game built on top of the simple +variant. + +The basic idea of the implementation is described in the WFLP 2010 +paper /Efficient and Compositional Higher-Order Streams/ +(<http://sgate.emt.bme.hu/documents/patai/publications/PataiWFLP2010.pdf>). + +In short, the basic idea is to define completely dynamic data-flow +networks through a pure combinator-style monadic interface. The +network can be turned into an I/O action that samples it sequentially +by the @start@ function. Under the hood, the network is represented +by mutable variables whose interconnections are encapsulated in +closures, and consistency is ensured by a two-phase update process +(essentially double buffering). The library keeps track of the +variables through weak pointers, so all of the live variables can be +updated (this is necessary to ensure referential transparency), and +unused ones can be garbage collected. + +-} + +module FRP.Elerea where +
− FRP/Elerea/Clocked.hs
@@ -1,822 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}--{-|--This version differs from the simple one in adding associated freeze-control signals (\'clocks\') to stateful entities to be able to pause-entire subnetworks without having to write all the low-level logic-explicitly. The clocks are fixed to signals upon their creation, and-the 'withClock' function can be used to specify the common clock for-the signals created in a given generator.--A clock signal affects 'delay' elements the following way: if the-clock signal is true, the delay works as usual, otherwise it remembers-its current output and throws away its current input. If we consider-signals to be functions of time (natural numbers), the behaviour of-delay can be described by the following function:--> delay x0 s (t_start,clk) t_sample-> | t_start == t_sample = x0-> | t_start < t_sample = if clk t_sample-> then s (t_sample-1)-> else delay x0 s t_start s_clock (t_sample-1)-> | otherwise = error "stream doesn't exist yet"--A simple example to create counters operating at different rates using-the same generator:--> divisibleBy n x = x `mod` n == 0->-> counter = stateful 0 (+1)->-> drift = do-> time <- counter-> c1 <- withClock (divisibleBy 2 <$> time) counter-> c2 <- withClock (divisibleBy 3 <$> time) counter-> return ((,) <$> c1 <*> c2)--Note that if you want to slow down the drift system defined above, the-naive approach might lead to surprising results:--> slowDrift = do-> time <- counter-> withClock (divisibleBy 2 <$> time) drift--The problem is that the clocks are also slowed down, and their spikes-double in length. This may or may not be what you want. To overcome-this problem, we can define a clock oblivious edge detector to be used-within the definition of @drift@:--> edge = withClock (pure True) . transfer False (\b b' -> b && not b')->-> drift = do-> time <- counter-> t2 <- edge (divisibleBy 2 <$> time)-> t3 <- edge (divisibleBy 3 <$> time)-> c1 <- withClock t2 counter-> c2 <- withClock t3 counter-> return ((,) <$> c1 <*> c2)--This works because the 'withClock' function overrides any clock-imposed on the generator from outside.---}--module FRP.Elerea.Clocked- (- -- * The signal abstraction- Signal- , SignalGen- -- * Embedding into I/O- , start- , external- , externalMulti- -- * Basic building blocks- , delay- , snapshot- , generator- , memo- , until- , withClock- , global- -- * Derived combinators- , stateful- , transfer- , transfer2- , transfer3- , transfer4- -- * Signals with side effects- -- $effectful- , execute- , effectful- , effectful1- , effectful2- , effectful3- , effectful4- ) where--import Control.Applicative-import Control.Concurrent.MVar-import Control.Monad-import Control.Monad.Base-import Control.Monad.Fix-import Control.Monad.IO.Class-import Data.IORef-import Data.Maybe-import Prelude hiding (until)-import System.Mem.Weak---- | A signal represents a value changing over time. It can be--- thought of as a function of type @Nat -> a@, where the argument is--- the sampling time, and the 'Monad' instance agrees with the--- intuition (bind corresponds to extracting the current sample).--- Signals and the values they carry are denoted the following way in--- the documentation:------ > s = <<s0 s1 s2 ...>>------ This says that @s@ is a signal that reads @s0@ during the first--- sampling, @s1@ during the second and so on. You can also think of--- @s@ as the following function:------ > s t_sample = [s0,s1,s2,...] !! t_sample------ Signals are constrained to be sampled sequentially, there is no--- random access. The only way to observe their output is through--- 'start'.-newtype Signal a = S (IO a) deriving (Functor, Applicative, Monad)---- | A pair of actions to update a signal in two phases: internal--- update without changing the output, finalisation (throwing away--- previous state).-type UpdateAction = (IO (), IO ())---- | A pointer to an update pair.-data Update = USig (Weak UpdateAction) -- ^ ordinary signal- | UClk UpdateAction -- ^ clocked subnetwork superstep---- | A dynamic set of actions to update a network without breaking--- consistency.-type UpdatePool = [Update]---- | A signal generator is the only source of stateful signals. It--- can be thought of as a function of type @Nat -> a@, where the--- result is an arbitrary data structure that can potentially contain--- new signals, and the argument is the creation time of these new--- signals. It exposes the 'MonadFix' interface, which makes it--- possible to define signals in terms of each other. Unlike the--- simple variant, the denotation of signal generators differs from--- that of signals. We will use the following notation for--- generators:------ > g = <|g0 g1 g2 ...|>------ Just like signals, generators behave as functions of time, but they--- can also refer to the clock signal:------ > g t_start s_clock = [g0,g1,g2,...] !! t_start------ The conceptual difference between the two notions is that signals--- are passed a sampling time, while generators expect a start time--- that will be the creation time of all the freshly generated--- signals in the resulting structure.-newtype SignalGen a = SG { unSG :: IORef UpdatePool -> IORef UpdatePool -> IO a }---- | The phases every signal goes through during a superstep.-data Phase a = Ready a | Updated a a--instance Functor SignalGen where- fmap = liftM--instance Applicative SignalGen where- pure = return- (<*>) = ap--instance Monad SignalGen where- return x = SG $ \_ _ -> return x- SG g >>= f = SG $ \p1 p2 -> g p1 p2 >>= \x -> unSG (f x) p1 p2--instance MonadFix SignalGen where- mfix f = SG $ \p1 p2 -> mfix $ \x -> unSG (f x) p1 p2--instance MonadIO SignalGen where- liftIO = execute--instance MonadBase SignalGen SignalGen where- liftBase = id--getUpdate :: Update -> IO (Maybe (Update, UpdateAction))-getUpdate upd@(USig ptr) = (fmap.fmap) ((,) upd) (deRefWeak ptr)-getUpdate upd@(UClk ua) = return (Just (upd,ua))---- | Embedding a signal into an 'IO' environment. Repeated calls to--- the computation returned cause the whole network to be updated, and--- the current sample of the top-level signal is produced as a--- result. This is the only way to extract a signal generator outside--- the network, and it is equivalent to passing zero to the function--- representing the generator. The clock associated with the--- top-level signal ticks at every sampling point. In general:------ > replicateM n =<< start <|<<x0 x1 x2 x3 ...>> ...|> == take n [x0,x1,x2,x3,...]------ Example:------ > do--- > smp <- start (stateful 3 (+2))--- > res <- replicateM 5 smp--- > print res------ Output:------ > [3,5,7,9,11]-start :: SignalGen (Signal a) -- ^ the generator of the top-level signal- -> IO (IO a) -- ^ the computation to sample the signal-start (SG gen) = do- pool <- newIORef []- S sample <- gen pool pool- return $ do- res <- sample- superstep pool- return res---- | Performing the two-phase superstep.-superstep :: IORef UpdatePool -> IO ()-superstep pool = loop id []- where- loop getPtrs final = do- (ptrs,acts) <- unzip.catMaybes <$> (mapM getUpdate =<< readIORef pool)- case acts of- [] -> do- sequence_ final- writeIORef pool (getPtrs [])- _ -> do- writeIORef pool []- mapM_ fst acts- loop ((ptrs++) . getPtrs) (mapM_ snd acts : final)---- | Auxiliary function used by all the primitives that create a--- mutable variable.-addSignal :: (a -> IO a) -- ^ sampling function- -> (a -> IO ()) -- ^ aging function- -> IORef (Phase a) -- ^ the mutable variable behind the signal- -> IORef UpdatePool -- ^ the pool of update actions- -> IO (Signal a) -- ^ the signal created-addSignal sample update ref pool = do- let upd = readIORef ref >>= \v -> case v of- Ready x -> update x- _ -> return ()-- fin = readIORef ref >>= \v -> case v of- Updated x _ -> writeIORef ref $! Ready x- _ -> error "Signal not updated!"-- sig = S $ readIORef ref >>= \v -> case v of- Ready x -> sample x- Updated _ x -> return x- {-# NOINLINE sig #-}- -- NOINLINE to prevent sig from getting inlined into the- -- argument position of mkWeak.-- updateActions <- mkWeak sig (upd,fin) Nothing- modifyIORef pool (USig updateActions:)- return sig---- | The 'delay' combinator is the elementary building block for--- adding state to the signal network by constructing delayed versions--- of a signal that emit a given value at creation time and the--- previous output of the signal afterwards.------ The clock signal associated to the generator affects 'delay'--- elements the following way: if the clock signal is true, the delay--- works as usual, otherwise it remembers its current output and--- throws away its current input. If we consider signals to be--- functions of time (natural numbers), the behaviour of delay can be--- described by the following function:------ > delay x0 s t_start s_clock t_sample--- > | t_start == t_sample = x0--- > | t_start < t_sample = if s_clock t_sample--- > then s (t_sample-1)--- > else delay x0 s t_start s_clock (t_sample-1)--- > | otherwise = error "stream doesn't exist yet"------ The way signal generators are extracted by 'generator' ensures that--- the error can never happen.------ Example (requires the @DoRec@ extension):------ > do--- > smp <- start $ do--- > rec let fib'' = liftA2 (+) fib' fib--- > fib' <- delay 1 fib''--- > fib <- delay 1 fib'--- > return fib--- > res <- replicateM 7 smp--- > print res------ Output:------ > [1,1,2,3,5,8,13]-delay :: a -- ^ initial output at creation time- -> Signal a -- ^ the signal to delay- -> SignalGen (Signal a) -- ^ the delayed signal-delay x0 (S s) = SG $ \_gpool pool -> do- ref <- newIORef (Ready x0)-- let update x = s >>= \x' -> x' `seq` writeIORef ref (Updated x' x)-- addSignal return update ref pool---- | A formal conversion from signals to signal generators, which--- effectively allows for retrieving the current value of a previously--- created signal within a generator. This includes both signals--- defined in an external scope as well as those created earlier in--- the same generator. It can be modelled by the following function:------ > snapshot s t_start s_clock = s t_start-snapshot :: Signal a -> SignalGen a-snapshot (S s) = SG $ \_ _ -> s---- | Auxiliary function.-memoise :: IORef (Phase a) -> a -> IO a-memoise ref x = writeIORef ref (Updated undefined x) >> return x---- | A reactive signal that takes the value to output from a signal--- generator carried by its input with the sampling time provided as--- the start time for the generated structure. It is possible to--- create new signals in the monad, which is the key to defining--- dynamic data-flow networks.------ > generator << <|x00 x01 x02 ...|>--- > <|x10 x11 x12 ...|>--- > <|x20 x21 x22 ...|>--- > ...--- > >> = <| <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > ...--- > |>------ It can be thought of as the following function:------ > generator g t_start s_clock t_sample = g t_sample s_clock t_sample------ It has to live in the 'SignalGen' monad, because it needs to--- maintain an internal state to be able to cache the current sample--- for efficiency reasons. However, this state is not carried between--- samples, therefore start time doesn't matter and can be ignored.--- Also, even though it does not make use of the clock itself, part of--- its job is to distribute it among the newly generated signals.------ Refer to the longer example at the bottom of "FRP.Elerea.Simple" to--- see how it can be used.-generator :: Signal (SignalGen a) -- ^ the signal of generators to run- -> SignalGen (Signal a) -- ^ the signal of generated structures-generator (S s) = SG $ \gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = (s >>= \(SG g) -> g gpool pool) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref gpool---- | Memoising combinator. It can be used to cache results of--- applicative combinators in case they are used in several places.--- Unlike in the simple variant, it is not observationally equivalent--- to 'return' in the 'SignalGen' monad, because it only samples its--- input signal when the associated clock ticks. The @memo@--- combinator can be modelled by the following function:------ > memo s t_start s_clock t_sample--- > | s_clock t_sample = s t_sample--- > | otherwise = memo s t_start s_clock (t_sample-1)------ For instance, if @s = f \<$\> s'@, then @f@ will be recalculated--- once for each sampling of @s@. This can be avoided by writing @s--- \<- memo (f \<$\> s')@ instead. However, 'memo' incurs a small--- overhead, therefore it should not be used blindly.------ All the functions defined in this module return memoised signals.-memo :: Signal a -- ^ the signal to cache- -> SignalGen (Signal a) -- ^ a signal observationally equivalent to the argument-memo (S s) = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that is true exactly once: the first time the input--- signal is true. Afterwards, it is constantly false, and it holds--- no reference to the input signal. Note that 'until' always follows--- the master clock, i.e. the fastest one, therefore it never creates--- a long spike of @True@. For instance (assuming the rest of the--- input is constantly @False@):------ > until <<False False True True False True ...>> =--- > <| <<False False True False False False False False False False ...>>--- > << --- False True False False False False False False False ...>>--- > << --- --- True False False False False False False False ...>>--- > << --- --- --- True False False False False False False ...>>--- > << --- --- --- --- False True False False False False ...>>--- > << --- --- --- --- --- True False False False False ...>>--- > << --- --- --- --- --- --- False False False False ...>>--- > ...--- > |>------ It is observationally equivalent to the following expression (which--- would hold onto @s@ forever):------ > until s = global $ do--- > step <- transfer False (||) s--- > dstep <- delay False step--- > memo (liftA2 (/=) step dstep)------ Example:------ > do--- > smp <- start $ do--- > cnt <- stateful 0 (+1)--- > tick <- until ((>=3) <$> cnt)--- > return $ liftA2 (,) cnt tick--- > res <- replicateM 6 smp--- > print res------ Output:------ > [(0,False),(1,False),(2,False),(3,True),(4,False),(5,False)]-until :: Signal Bool -- ^ the boolean input signal- -> SignalGen (Signal Bool) -- ^ a one-shot signal true only the first time the input is true-until (S s) = SG $ \gpool _pool -> do- ref <- newIORef (Ready undefined)-- rsmp <- mfix $ \rs -> newIORef $ do- x <- s- writeIORef ref (Updated undefined x)- when x $ writeIORef rs $ do- writeIORef ref (Updated undefined False)- return False- return x-- let sample = join (readIORef rsmp)-- addSignal (const sample) (const (() <$ sample)) ref gpool---- | Override the clock used in a generator. Note that clocks don't--- interact unless one is used in the definition of the other, i.e. it--- is possible to provide a fast clock within a generator with a slow--- associated clock. It is equivalent to the following function:------ > withClock s g t_start s_clock = g t_start s------ For instance, the following equivalence holds:------ > withClock (pure False) (stateful x f) == pure x-withClock :: Signal Bool -> SignalGen a -> SignalGen a-withClock (S cs) (SG g) = SG $ \gpool _pool -> do- pool' <- newIORef []- pref <- newIORef Nothing-- let whenc act = cs >>= flip when act-- upd = readIORef pref >>= \mp -> case mp of- Nothing -> do- (ptrs,acts) <- unzip.catMaybes <$> (mapM getUpdate =<< readIORef pool')- writeIORef pool' ptrs- writeIORef pref (Just acts)- mapM_ fst acts- Just _ -> return ()-- fin = readIORef pref >>= \mp -> case mp of- Nothing -> return ()- Just acts -> do- writeIORef pref Nothing- mapM_ snd acts-- modifyIORef gpool (UClk (whenc upd, whenc fin):)- g gpool pool'---- | Equivalent to @withClock (pure True)@, but more efficient.-global :: SignalGen a -> SignalGen a-global (SG g) = SG $ \gpool _ -> g gpool gpool---- | A signal that can be directly fed through the sink function--- returned. This can be used to attach the network to the outer--- world. The signal always yields the value last written to the--- sink. In other words, if the sink is written less frequently than--- the network sampled, the output remains the same during several--- samples. If values are pushed in the sink more frequently, only--- the last one before sampling is visible on the output.------ Example:------ > do--- > (sig,snk) <- external 4--- > smp <- start (return sig)--- > r1 <- smp--- > r2 <- smp--- > snk 7--- > r3 <- smp--- > snk 9--- > snk 2--- > r4 <- smp--- > print [r1,r2,r3,r4]------ Output:------ > [4,4,7,2]-external :: a -- ^ initial value- -> IO (Signal a, a -> IO ()) -- ^ the signal and an IO function to feed it-external x = do- ref <- newIORef x- return (S (readIORef ref), writeIORef ref)---- | An event-like signal that can be fed through the sink function--- returned. The signal carries a list of values fed in since the--- last sampling (always synchronised to the top-level samplings--- regardless of any associated clock), i.e. it is constantly @[]@ if--- the sink is never invoked. The order of elements is reversed, so--- the last value passed to the sink is the head of the list. Note--- that unlike 'external' this function only returns a generator to be--- used within the expression constructing the top-level stream, and--- this generator can only be used once.------ Example:------ > do--- > (gen,snk) <- externalMulti--- > smp <- start gen--- > r1 <- smp--- > snk 7--- > r2 <- smp--- > r3 <- smp--- > snk 9--- > snk 2--- > r4 <- smp--- > print [r1,r2,r3,r4]------ Output:------ > [[],[7],[],[2,9]]-externalMulti :: IO (SignalGen (Signal [a]), a -> IO ()) -- ^ a generator for the event signal and the associated sink-externalMulti = do- var <- newMVar []- return (SG $ \gpool _pool -> do- ref <- newIORef (Ready undefined)- let sample = modifyMVar var $ \list -> memoise ref list >> return ([], list)- addSignal (const sample) (const (() <$ sample)) ref gpool- ,\val -> do- vals <- takeMVar var- putMVar var (val:vals)- )---- | A pure stateful signal. The initial state is the first output,--- and every subsequent state is derived from the preceding one by--- applying a pure transformation. It is affected by the associated--- clock like 'delay': no transformation is performed in the absence--- of a tick; see the example at the top.------ Example:------ > do--- > smp <- start (stateful "x" ('x':))--- > res <- replicateM 5 smp--- > print res------ Output:------ > ["x","xx","xxx","xxxx","xxxxx"]-stateful :: a -- ^ initial state- -> (a -> a) -- ^ state transformation- -> SignalGen (Signal a)-stateful x0 f = mfix $ \sig -> delay x0 (f <$> sig)---- | A stateful transfer function. The current input affects the--- current output, i.e. the initial state given in the first argument--- is considered to appear before the first output, and can never be--- observed, and subsequent states are determined by combining the--- preceding state with the current output of the input signal using--- the function supplied. It is affected by the associated clock like--- 'delay': no transformation is performed in the absence of a tick;--- see the example at the top.------ Example:------ > do--- > smp <- start $ do--- > cnt <- stateful 1 (+1)--- > transfer 10 (+) cnt--- > res <- replicateM 5 smp--- > print res------ Output:------ > [11,13,16,20,25]-transfer :: a -- ^ initial internal state- -> (t -> a -> a) -- ^ state updater function- -> Signal t -- ^ input signal- -> SignalGen (Signal a)-transfer x0 f s = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA2 f s sig')---- | A variation of 'transfer' with two input signals.-transfer2 :: a -- ^ initial internal state- -> (t1 -> t2 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> SignalGen (Signal a)-transfer2 x0 f s1 s2 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA3 f s1 s2 sig')---- | A variation of 'transfer' with three input signals.-transfer3 :: a -- ^ initial internal state- -> (t1 -> t2 -> t3 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> SignalGen (Signal a)-transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM4 f s1 s2 s3 sig')---- | A variation of 'transfer' with four input signals.-transfer4 :: a -- ^ initial internal state- -> (t1 -> t2 -> t3 -> t4 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> Signal t4 -- ^ input signal 4- -> SignalGen (Signal a)-transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM5 f s1 s2 s3 s4 sig')--{- $effectful--The following combinators are primarily aimed at library implementors-who wish build abstractions to effectful libraries on top of Elerea.---}---- | An IO action executed in the 'SignalGen' monad. Can be used as--- `liftIO`.-execute :: IO a -> SignalGen a-execute act = SG $ \_ _ -> act---- | A signal that executes a given IO action once at every sampling.------ In essence, this combinator provides cooperative multitasking--- capabilities, and its primary purpose is to assist library writers--- in wrapping effectful APIs as conceptually pure signals. If there--- are several effectful signals in the system, their order of--- execution is undefined and should not be relied on.------ Example:------ > do--- > smp <- start $ do--- > ref <- execute $ newIORef 0--- > effectful $ do--- > x <- readIORef ref--- > putStrLn $ "Count: " ++ show x--- > writeIORef ref $! x+1--- > return ()--- > replicateM_ 5 smp------ Output:------ > Count: 0--- > Count: 1--- > Count: 2--- > Count: 3--- > Count: 4------ Another example (requires mersenne-random):------ > do--- > smp <- start $ effectful $ return randomIO :: IO (IO Double)--- > res <- replicateM 5 smp--- > print res------ Output:------ > [0.12067753390401374,0.8658877349182655,0.7159264443196786,0.1756941896012891,0.9513646060896676]-effectful :: IO a -- ^ the action to be executed repeatedly- -> SignalGen (Signal a)-effectful act = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that executes a parametric IO action once at every--- sampling. The parameter is supplied by another signal at every--- sampling step.-effectful1 :: (t -> IO a) -- ^ the action to be executed repeatedly- -> Signal t -- ^ parameter signal- -> SignalGen (Signal a)-effectful1 act (S s) = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with two parameter signals.-effectful2 :: (t1 -> t2 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> SignalGen (Signal a)-effectful2 act (S s1) (S s2) = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM2 act s1 s2) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with three parameter signals.-effectful3 :: (t1 -> t2 -> t3 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> SignalGen (Signal a)-effectful3 act (S s1) (S s2) (S s3) = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM3 act s1 s2 s3) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with four parameter signals.-effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> Signal t4 -- ^ parameter signal 4- -> SignalGen (Signal a)-effectful4 act (S s1) (S s2) (S s3) (S s4) = SG $ \_gpool pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM4 act s1 s2 s3 s4) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool--instance Show (Signal a) where- showsPrec _ _ s = "<SIGNAL>" ++ s--instance Eq (Signal a) where- _ == _ = False---- | Error message for unimplemented instance functions.-unimp :: String -> a-unimp = error . ("Signal: "++)--instance Ord t => Ord (Signal t) where- compare = unimp "compare"- min = liftA2 min- max = liftA2 max--instance Enum t => Enum (Signal t) where- succ = fmap succ- pred = fmap pred- toEnum = pure . toEnum- fromEnum = unimp "fromEnum"- enumFrom = unimp "enumFrom"- enumFromThen = unimp "enumFromThen"- enumFromTo = unimp "enumFromTo"- enumFromThenTo = unimp "enumFromThenTo"--instance Bounded t => Bounded (Signal t) where- minBound = pure minBound- maxBound = pure maxBound--instance Num t => Num (Signal t) where- (+) = liftA2 (+)- (-) = liftA2 (-)- (*) = liftA2 (*)- signum = fmap signum- abs = fmap abs- negate = fmap negate- fromInteger = pure . fromInteger--instance Real t => Real (Signal t) where- toRational = unimp "toRational"--instance Integral t => Integral (Signal t) where- quot = liftA2 quot- rem = liftA2 rem- div = liftA2 div- mod = liftA2 mod- quotRem a b = (fst <$> qrab,snd <$> qrab)- where qrab = quotRem <$> a <*> b- divMod a b = (fst <$> dmab,snd <$> dmab)- where dmab = divMod <$> a <*> b- toInteger = unimp "toInteger"--instance Fractional t => Fractional (Signal t) where- (/) = liftA2 (/)- recip = fmap recip- fromRational = pure . fromRational--instance Floating t => Floating (Signal t) where- pi = pure pi- exp = fmap exp- sqrt = fmap sqrt- log = fmap log- (**) = liftA2 (**)- logBase = liftA2 logBase- sin = fmap sin- tan = fmap tan- cos = fmap cos- asin = fmap asin- atan = fmap atan- acos = fmap acos- sinh = fmap sinh- tanh = fmap tanh- cosh = fmap cosh- asinh = fmap asinh- atanh = fmap atanh- acosh = fmap acosh
FRP/Elerea/Param.hs view
@@ -1,733 +1,752 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}--{-|--This module provides leak-free and referentially transparent-higher-order discrete signals. Unlike in "FRP.Elerea.Simple", the-sampling action has an extra argument that will be globally-distributed to every node and can be used to update the state. For-instance, it can hold the time step between the two samplings, but it-could also encode all the external input to the system.---}--module FRP.Elerea.Param- (- -- * The signal abstraction- Signal- , SignalGen- -- * Embedding into I/O- , start- , external- , externalMulti- -- * Basic building blocks- , delay- , snapshot- , generator- , memo- , until- , input- , embed- -- * Derived combinators- , stateful- , transfer- , transfer2- , transfer3- , transfer4- -- * Signals with side effects- -- $effectful- , execute- , effectful- , effectful1- , effectful2- , effectful3- , effectful4- ) where--import Control.Applicative-import Control.Concurrent.MVar-import Control.Monad-import Control.Monad.Base-import Control.Monad.Fix-import Control.Monad.IO.Class-import Data.IORef-import Data.Maybe-import Prelude hiding (until)-import System.Mem.Weak---- | A signal represents a value changing over time. It can be--- thought of as a function of type @Nat -> a@, where the argument is--- the sampling time, and the 'Monad' instance agrees with the--- intuition (bind corresponds to extracting the current sample).--- Signals and the values they carry are denoted the following way in--- the documentation:------ > s = <<s0 s1 s2 ...>>------ This says that @s@ is a signal that reads @s0@ during the first--- sampling, @s1@ during the second and so on. You can also think of--- @s@ as the following function:------ > s t_sample = [s0,s1,s2,...] !! t_sample------ Signals are constrained to be sampled sequentially, there is no--- random access. The only way to observe their output is through--- 'start'.-newtype Signal a = S (IO a) deriving (Functor, Applicative, Monad)---- | A dynamic set of actions to update a network without breaking--- consistency.-type UpdatePool = [Weak (IO (), IO ())]---- | A signal generator is the only source of stateful signals. It--- can be thought of as a function of type @Nat -> Signal p -> a@,--- where the result is an arbitrary data structure that can--- potentially contain new signals, the first argument is the creation--- time of these new signals, and the second is a globally accessible--- input signal. It exposes the 'MonadFix' interface, which makes it--- possible to define signals in terms of each other. Unlike the--- simple variant, the denotation of signal generators differs from--- that of signals. We will use the following notation for--- generators:------ > g = <|g0 g1 g2 ...|>------ Just like signals, generators behave as functions of time, but they--- can also refer to the input signal:------ > g t_start s_input = [g0,g1,g2,...] !! t_start------ The conceptual difference between the two notions is that signals--- are passed a sampling time, while generators expect a start time--- that will be the creation time of all the freshly generated--- signals in the resulting structure.-newtype SignalGen p a = SG { unSG :: IORef UpdatePool -> Signal p -> IO a }---- | The phases every signal goes through during a superstep.-data Phase a = Ready a | Updated a a--instance Functor (SignalGen p) where- fmap = liftM--instance Applicative (SignalGen p) where- pure = return- (<*>) = ap--instance Monad (SignalGen p) where- return x = SG $ \_ _ -> return x- SG g >>= f = SG $ \p i -> g p i >>= \x -> unSG (f x) p i--instance MonadFix (SignalGen p) where- mfix f = SG $ \p i -> mfix $ \x -> unSG (f x) p i--instance MonadIO (SignalGen p) where- liftIO = execute--instance MonadBase (SignalGen p) (SignalGen p) where- liftBase = id---- | Embedding a signal into an 'IO' environment. Repeated calls to--- the computation returned cause the whole network to be updated, and--- the current sample of the top-level signal is produced as a result.--- The computation accepts a global parameter that will be distributed--- to all signals. For instance, this can be the time step, if we--- want to model continuous-time signals. This is the only way to--- extract a signal generator outside the network, and it is--- equivalent to passing zero to the function representing the--- generator.------ Example:------ > do--- > smp <- start (stateful 10 (+))--- > res <- forM [5,3,2,9,4] smp--- > print res------ Output:------ > [10,15,18,20,29]-start :: SignalGen p (Signal a) -- ^ the generator of the top-level signal- -> IO (p -> IO a) -- ^ the computation to sample the signal-start (SG gen) = do- pool <- newIORef []- (inp,sink) <- external undefined- S sample <- gen pool inp- return $ \param -> do- sink param- res <- sample- superstep pool- return res---- | Performing the two-phase superstep.-superstep :: IORef UpdatePool -> IO ()-superstep pool = loop id []- where- deref ptr = (fmap.fmap) ((,) ptr) (deRefWeak ptr)- loop getPtrs final = do- (ptrs,acts) <- unzip.catMaybes <$> (mapM deref =<< readIORef pool)- case acts of- [] -> do- sequence_ final- writeIORef pool (getPtrs [])- _ -> do- writeIORef pool []- mapM_ fst acts- loop ((ptrs++) . getPtrs) (mapM_ snd acts : final)---- | Auxiliary function used by all the primitives that create a--- mutable variable.-addSignal :: (a -> IO a) -- ^ sampling function- -> (a -> IO ()) -- ^ aging function- -> IORef (Phase a) -- ^ the mutable variable behind the signal- -> IORef UpdatePool -- ^ the pool of update actions- -> IO (Signal a)-addSignal sample update ref pool = do- let upd = readIORef ref >>= \v -> case v of- Ready x -> update x- _ -> return ()-- fin = readIORef ref >>= \v -> case v of- Updated x _ -> writeIORef ref $! Ready x- _ -> error "Signal not updated!"-- sig = S $ readIORef ref >>= \v -> case v of- Ready x -> sample x- Updated _ x -> return x- {-# NOINLINE sig #-}- -- NOINLINE to prevent sig from getting inlined into the- -- argument position of mkWeak.-- updateActions <- mkWeak sig (upd,fin) Nothing- modifyIORef pool (updateActions:)- return sig---- | The 'delay' combinator is the elementary building block for--- adding state to the signal network by constructing delayed versions--- of a signal that emit a given value at creation time and the--- previous output of the signal afterwards (@--@ is undefined):------ > delay x0 s = <| <<x0 s0 s1 s2 s3 ...>>--- > <<-- x0 s1 s2 s3 ...>>--- > <<-- -- x0 s2 s3 ...>>--- > <<-- -- -- x0 s3 ...>>--- > ...--- > |>------ It can be thought of as the following function (which should also--- make it clear why the return value is 'SignalGen'):------ > delay x0 s t_start s_input t_sample--- > | t_start == t_sample = x0--- > | t_start < t_sample = s (t_sample-1)--- > | otherwise = error \"Premature sample!\"------ The way signal generators are extracted by 'generator' ensures that--- the error can never happen. It is also clear that the behaviour of--- 'delay' is not affected in any way by the global input.------ Example (requires the @DoRec@ extension):------ > do--- > smp <- start $ do--- > rec let fib'' = liftA2 (+) fib' fib--- > fib' <- delay 1 fib''--- > fib <- delay 1 fib'--- > return fib--- > res <- replicateM 7 (smp undefined)--- > print res------ Output:------ > [1,1,2,3,5,8,13]-delay :: a -- ^ initial output- -> Signal a -- ^ the signal to delay- -> SignalGen p (Signal a)-delay x0 (S s) = SG $ \pool _ -> do- ref <- newIORef (Ready x0)-- let update x = s >>= \x' -> x' `seq` writeIORef ref (Updated x' x)-- addSignal return update ref pool---- | A formal conversion from signals to signal generators, which--- effectively allows for retrieving the current value of a previously--- created signal within a generator. This includes both signals--- defined in an external scope as well as those created earlier in--- the same generator. It can be modelled by the following function:------ > snapshot s t_start s_input = s t_start-snapshot :: Signal a -> SignalGen p a-snapshot (S s) = SG $ \_ _ -> s---- | Auxiliary function.-memoise :: IORef (Phase a) -> a -> IO a-memoise ref x = writeIORef ref (Updated undefined x) >> return x---- | A reactive signal that takes the value to output from a signal--- generator carried by its input with the sampling time provided as--- the start time for the generated structure. It is possible to--- create new signals in the monad, which is the key to defining--- dynamic data-flow networks.------ > generator << <|x00 x01 x02 ...|>--- > <|x10 x11 x12 ...|>--- > <|x20 x21 x22 ...|>--- > ...--- > >> = <| <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > ...--- > |>------ It can be thought of as the following function:------ > generator g t_start s_input t_sample = g t_sample t_sample s_input------ It has to live in the 'SignalGen' monad, because it needs to--- maintain an internal state to be able to cache the current sample--- for efficiency reasons. However, this state is not carried between--- samples, therefore start time doesn't matter and can be ignored.--- Also, even though it does not make use of the global input itself,--- part of its job is to distribute it among the newly generated--- signals.------ Refer to the longer example at the bottom of "FRP.Elerea.Simple" to--- see how it can be used.-generator :: Signal (SignalGen p a) -- ^ the signal of generators to run- -> SignalGen p (Signal a) -- ^ the signal of generated structures-generator (S s) = SG $ \pool inp -> do- ref <- newIORef (Ready undefined)-- let sample = (s >>= \(SG g) -> g pool inp) >>= memoise ref- - addSignal (const sample) (const (() <$ sample)) ref pool---- | Memoising combinator. It can be used to cache results of--- applicative combinators in case they are used in several places.--- It is observationally equivalent to 'return' in the 'SignalGen'--- monad.------ > memo s = <|s s s s ...|>------ For instance, if @s = f \<$\> s'@, then @f@ will be recalculated--- once for each sampling of @s@. This can be avoided by writing @s--- \<- memo (f \<$\> s')@ instead. However, 'memo' incurs a small--- overhead, therefore it should not be used blindly.------ All the functions defined in this module return memoised signals.--- Just like 'delay', it is independent of the global input.-memo :: Signal a -- ^ the signal to cache- -> SignalGen p (Signal a) -- ^ a signal observationally equivalent to the argument-memo (S s) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that is true exactly once: the first time the input--- signal is true. Afterwards, it is constantly false, and it holds--- no reference to the input signal. For instance (assuming the rest--- of the input is constantly @False@):------ > until <<False False True True False True ...>> =--- > <| <<False False True False False False False False False False ...>>--- > << --- False True False False False False False False False ...>>--- > << --- --- True False False False False False False False ...>>--- > << --- --- --- True False False False False False False ...>>--- > << --- --- --- --- False True False False False False ...>>--- > << --- --- --- --- --- True False False False False ...>>--- > << --- --- --- --- --- --- False False False False ...>>--- > ...--- > |>------ It is observationally equivalent to the following expression (which--- would hold onto @s@ forever):------ > until s = do--- > step <- transfer False (const (||)) s--- > dstep <- delay False step--- > memo (liftA2 (/=) step dstep)------ Example:------ > do--- > smp <- start $ do--- > accum <- stateful 0 (+)--- > tick <- until ((>=10) <$> accum)--- > return $ liftA2 (,) accum tick--- > res <- forM [4,1,3,5,2,8,6] smp--- > print res------ Output:------ > [(0,False),(4,False),(5,False),(8,False),(13,True),(15,False),(23,False)]-until :: Signal Bool -- ^ the boolean input signal- -> SignalGen p (Signal Bool) -- ^ a one-shot signal true only the first time the input is true-until (S s) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- rsmp <- mfix $ \rs -> newIORef $ do- x <- s- writeIORef ref (Updated undefined x)- when x $ writeIORef rs $ do- writeIORef ref (Updated undefined False)- return False- return x-- let sample = join (readIORef rsmp)-- addSignal (const sample) (const (() <$ sample)) ref pool---- | The common input signal that is fed through the function returned--- by 'start', unless we are in an 'embed'ded generator. It is--- equivalent to the following function:------ > input t_start s_input = s_input------ Example:------ > do--- > smp <- start $ do--- > sig <- input--- > return (sig*2)--- > res <- forM [4,1,3,5,2,8,6] smp--- > print res------ Output:------ > [8,2,6,10,4,16,12]-input :: SignalGen p (Signal p)-input = SG $ const return---- | Embed a generator with an overridden input signal. It is--- equivalent to the following function:------ > embed s g t_start s_input = g t_start s------ Example:------ > do--- > smp <- start $ do--- > sig <- input--- > embed (sig*2) $ do--- > sig <- input--- > return (sig+1)--- > res <- forM [4,1,3,5,2,8,6] smp--- > print res------ Output:------ > [9,3,7,11,5,17,13]-embed :: Signal p' -> SignalGen p' a -> SignalGen p a-embed s (SG g) = SG $ \pool _ -> g pool s---- | A signal that can be directly fed through the sink function--- returned. This can be used to attach the network to the outer--- world. Note that this is optional, as all the input of the network--- can be fed in through the global parameter, although that is not--- really convenient for many signals.-external :: a -- ^ initial value- -> IO (Signal a, a -> IO ()) -- ^ the signal and an IO function to feed it-external x = do- ref <- newIORef x- return (S (readIORef ref), writeIORef ref)---- | An event-like signal that can be fed through the sink function--- returned. The signal carries a list of values fed in since the--- last sampling, i.e. it is constantly [] if the sink is never--- invoked. The order of elements is reversed, so the last value--- passed to the sink is the head of the list. Note that unlike--- 'external' this function only returns a generator to be used within--- the expression constructing the top-level stream, and this--- generator can only be used once.-externalMulti :: IO (SignalGen p (Signal [a]), a -> IO ()) -- ^ a generator for the event signal and the associated sink-externalMulti = do- var <- newMVar []- return (SG $ \pool _ -> do- ref <- newIORef (Ready undefined)- let sample = modifyMVar var $ \list -> memoise ref list >> return ([], list)- addSignal (const sample) (const (() <$ sample)) ref pool- ,\val -> do vals <- takeMVar var- putMVar var (val:vals)- )---- | A direct stateful transformation of the input. The initial state--- is the first output, and every following output is calculated from--- the previous one and the value of the global parameter (which might--- have been overridden by 'embed').------ Example:------ > do--- > smp <- start (stateful "" (:))--- > res <- forM "olleh~" smp--- > print res------ Output:------ > ["","o","lo","llo","ello","hello"]-stateful :: a -- ^ initial state- -> (p -> a -> a) -- ^ state transformation- -> SignalGen p (Signal a)-stateful x0 f = mfix $ \sig -> input >>= \i -> delay x0 (f <$> i <*> sig)---- | A stateful transfer function. The current input affects the--- current output, i.e. the initial state given in the first argument--- is considered to appear before the first output, and can never be--- observed. Every output is derived from the current value of the--- input signal, the global parameter (which might have been--- overridden by 'embed') and the previous output. It is equivalent--- to the following expression:------ Example (assuming a delta time is passed to the sampling function--- in each step):------ > integral x0 s = transfer x0 (\dt v x -> x+dt*v)------ Example for using the above:------ > do--- > smp <- start (integral 3 (pure 2))--- > res <- replicateM 7 (smp 0.1)--- > print res------ Output:------ > [3.2,3.4,3.6,3.8,4.0,4.2,4.4]-transfer :: a -- ^ initial internal state- -> (p -> t -> a -> a) -- ^ state updater function- -> Signal t -- ^ input signal- -> SignalGen p (Signal a)-transfer x0 f s = mfix $ \sig -> do- inp <- input- sig' <- delay x0 sig- memo (liftA3 f inp s sig')---- | A variation of 'transfer' with two input signals.-transfer2 :: a -- ^ initial internal state- -> (p -> t1 -> t2 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> SignalGen p (Signal a)-transfer2 x0 f s1 s2 = mfix $ \sig -> do- inp <- input- sig' <- delay x0 sig- memo (liftM4 f inp s1 s2 sig')---- | A variation of 'transfer' with three input signals.-transfer3 :: a -- ^ initial internal state- -> (p -> t1 -> t2 -> t3 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> SignalGen p (Signal a)-transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do- inp <- input- sig' <- delay x0 sig- memo (liftM5 f inp s1 s2 s3 sig')---- | A variation of 'transfer' with four input signals.-transfer4 :: a -- ^ initial internal state- -> (p -> t1 -> t2 -> t3 -> t4 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> Signal t4 -- ^ input signal 4- -> SignalGen p (Signal a)-transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do- inp <- input- sig' <- delay x0 sig- memo (liftM5 f inp s1 s2 s3 s4 `ap` sig')--{- $effectful--The following combinators are primarily aimed at library implementors-who wish build abstractions to effectful libraries on top of Elerea.---}---- | An IO action executed in the 'SignalGen' monad. Can be used as--- `liftIO`.-execute :: IO a -> SignalGen p a-execute act = SG $ \_ _ -> act---- | A signal that executes a given IO action once at every sampling.------ In essence, this combinator provides cooperative multitasking--- capabilities, and its primary purpose is to assist library writers--- in wrapping effectful APIs as conceptually pure signals. If there--- are several effectful signals in the system, their order of--- execution is undefined and should not be relied on.------ Example:------ > do--- > act <- start $ do--- > ref <- execute $ newIORef 0--- > let accum n = do--- > x <- readIORef ref--- > putStrLn $ "Accumulator: " ++ show x--- > writeIORef ref $! x+n--- > return ()--- > effectful1 accum =<< input--- > forM_ [4,9,2,1,5] act------ Output:------ > Accumulator: 0--- > Accumulator: 4--- > Accumulator: 13--- > Accumulator: 15--- > Accumulator: 16------ Another example (requires mersenne-random):------ > do--- > smp <- start $ effectful randomIO :: IO (IO Double)--- > res <- replicateM 5 smp--- > print res------ Output:------ > [0.12067753390401374,0.8658877349182655,0.7159264443196786,0.1756941896012891,0.9513646060896676]-effectful :: IO a -- ^ the action to be executed repeatedly- -> SignalGen p (Signal a)-effectful act = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that executes a parametric IO action once at every--- sampling. The parameter is supplied by another signal at every--- sampling step.-effectful1 :: (t -> IO a) -- ^ the action to be executed repeatedly- -> Signal t -- ^ parameter signal- -> SignalGen p (Signal a)-effectful1 act (S s) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with two parameter signals.-effectful2 :: (t1 -> t2 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> SignalGen p (Signal a)-effectful2 act (S s1) (S s2) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM2 act s1 s2) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with three parameter signals.-effectful3 :: (t1 -> t2 -> t3 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> SignalGen p (Signal a)-effectful3 act (S s1) (S s2) (S s3) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM3 act s1 s2 s3) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with four parameter signals.-effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> Signal t4 -- ^ parameter signal 4- -> SignalGen p (Signal a)-effectful4 act (S s1) (S s2) (S s3) (S s4) = SG $ \pool _ -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM4 act s1 s2 s3 s4) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | The @Show@ instance is only defined for the sake of 'Num'...-instance Show (Signal a) where- showsPrec _ _ s = "<SIGNAL>" ++ s---- | Equality test is impossible.-instance Eq (Signal a) where- _ == _ = False---- | Error message for unimplemented instance functions.-unimp :: String -> a-unimp = error . ("Signal: "++)--instance Ord t => Ord (Signal t) where- compare = unimp "compare"- min = liftA2 min- max = liftA2 max--instance Enum t => Enum (Signal t) where- succ = fmap succ- pred = fmap pred- toEnum = pure . toEnum- fromEnum = unimp "fromEnum"- enumFrom = unimp "enumFrom"- enumFromThen = unimp "enumFromThen"- enumFromTo = unimp "enumFromTo"- enumFromThenTo = unimp "enumFromThenTo"--instance Bounded t => Bounded (Signal t) where- minBound = pure minBound- maxBound = pure maxBound--instance Num t => Num (Signal t) where- (+) = liftA2 (+)- (-) = liftA2 (-)- (*) = liftA2 (*)- signum = fmap signum- abs = fmap abs- negate = fmap negate- fromInteger = pure . fromInteger--instance Real t => Real (Signal t) where- toRational = unimp "toRational"--instance Integral t => Integral (Signal t) where- quot = liftA2 quot- rem = liftA2 rem- div = liftA2 div- mod = liftA2 mod- quotRem a b = (fst <$> qrab,snd <$> qrab)- where qrab = quotRem <$> a <*> b- divMod a b = (fst <$> dmab,snd <$> dmab)- where dmab = divMod <$> a <*> b- toInteger = unimp "toInteger"--instance Fractional t => Fractional (Signal t) where- (/) = liftA2 (/)- recip = fmap recip- fromRational = pure . fromRational--instance Floating t => Floating (Signal t) where- pi = pure pi- exp = fmap exp- sqrt = fmap sqrt- log = fmap log- (**) = liftA2 (**)- logBase = liftA2 logBase- sin = fmap sin- tan = fmap tan- cos = fmap cos- asin = fmap asin- atan = fmap atan- acos = fmap acos- sinh = fmap sinh- tanh = fmap tanh- cosh = fmap cosh- asinh = fmap asinh- atanh = fmap atanh- acosh = fmap acosh+{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} + +{-| + +This module provides leak-free and referentially transparent +higher-order discrete signals. Unlike in "FRP.Elerea.Simple", the +sampling action has an extra argument that will be globally +distributed to every node and can be used to update the state. For +instance, it can hold the time step between the two samplings, but it +could also encode all the external input to the system. + +-} + +module FRP.Elerea.Param + ( + -- * The signal abstraction + Signal + , SignalGen + -- * Embedding into I/O + , start + , external + , externalMulti + , unsafeExternal + -- * Basic building blocks + , delay + , snapshot + , generator + , memo + , till + , input + , embed + -- * Derived combinators + , stateful + , transfer + , transfer2 + , transfer3 + , transfer4 + -- * Signals with side effects + -- $effectful + , execute + , effectful + , effectful1 + , effectful2 + , effectful3 + , effectful4 + ) where + +import Control.Applicative +import Control.Concurrent.MVar +import Control.Monad +import Control.Monad.Base +import Control.Monad.Fix +import Control.Monad.IO.Class +import Data.IORef +import Data.Maybe +import System.Mem.Weak + +-- | A signal represents a value changing over time. It can be +-- thought of as a function of type @Nat -> a@, where the argument is +-- the sampling time, and the 'Monad' instance agrees with the +-- intuition (bind corresponds to extracting the current sample). +-- Signals and the values they carry are denoted the following way in +-- the documentation: +-- +-- > s = <<s0 s1 s2 ...>> +-- +-- This says that @s@ is a signal that reads @s0@ during the first +-- sampling, @s1@ during the second and so on. You can also think of +-- @s@ as the following function: +-- +-- > s t_sample = [s0,s1,s2,...] !! t_sample +-- +-- Signals are constrained to be sampled sequentially, there is no +-- random access. The only way to observe their output is through +-- 'start'. +newtype Signal a = S (IO a) deriving (Functor, Applicative, Monad) + +-- | A dynamic set of actions to update a network without breaking +-- consistency. +type UpdatePool = [Weak (IO (), IO ())] + +-- | A signal generator is the only source of stateful signals. It +-- can be thought of as a function of type @Nat -> Signal p -> a@, +-- where the result is an arbitrary data structure that can +-- potentially contain new signals, the first argument is the creation +-- time of these new signals, and the second is a globally accessible +-- input signal. It exposes the 'MonadFix' interface, which makes it +-- possible to define signals in terms of each other. Unlike the +-- simple variant, the denotation of signal generators differs from +-- that of signals. We will use the following notation for +-- generators: +-- +-- > g = <|g0 g1 g2 ...|> +-- +-- Just like signals, generators behave as functions of time, but they +-- can also refer to the input signal: +-- +-- > g t_start s_input = [g0,g1,g2,...] !! t_start +-- +-- The conceptual difference between the two notions is that signals +-- are passed a sampling time, while generators expect a start time +-- that will be the creation time of all the freshly generated +-- signals in the resulting structure. +newtype SignalGen p a = SG { unSG :: IORef UpdatePool -> Signal p -> IO a } + +-- | The phases every signal goes through during a superstep. +data Phase a = Ready a | Updated a a + +instance Functor (SignalGen p) where + fmap = liftM + +instance Applicative (SignalGen p) where + pure = return + (<*>) = ap + +instance Monad (SignalGen p) where + return x = SG $ \_ _ -> return x + SG g >>= f = SG $ \p i -> g p i >>= \x -> unSG (f x) p i + +instance MonadFix (SignalGen p) where + mfix f = SG $ \p i -> mfix $ \x -> unSG (f x) p i + +instance MonadIO (SignalGen p) where + liftIO = execute + +instance MonadBase (SignalGen p) (SignalGen p) where + liftBase = id + +-- | Embedding a signal into an 'IO' environment. Repeated calls to +-- the computation returned cause the whole network to be updated, and +-- the current sample of the top-level signal is produced as a result. +-- The computation accepts a global parameter that will be distributed +-- to all signals. For instance, this can be the time step, if we +-- want to model continuous-time signals. This is the only way to +-- extract a signal generator outside the network, and it is +-- equivalent to passing zero to the function representing the +-- generator. +-- +-- Example: +-- +-- > do +-- > smp <- start (stateful 10 (+)) +-- > res <- forM [5,3,2,9,4] smp +-- > print res +-- +-- Output: +-- +-- > [10,15,18,20,29] +start :: SignalGen p (Signal a) -- ^ the generator of the top-level signal + -> IO (p -> IO a) -- ^ the computation to sample the signal +start (SG gen) = do + pool <- newIORef [] + (inp,sink) <- unsafeExternal undefined + S sample <- gen pool inp + return $ \param -> do + sink param + res <- sample + superstep pool + return res + +-- | Performing the two-phase superstep. +superstep :: IORef UpdatePool -> IO () +superstep pool = loop id [] + where + deref ptr = (fmap.fmap) ((,) ptr) (deRefWeak ptr) + loop getPtrs final = do + (ptrs,acts) <- unzip.catMaybes <$> (mapM deref =<< readIORef pool) + case acts of + [] -> do + sequence_ final + writeIORef pool (getPtrs []) + _ -> do + writeIORef pool [] + mapM_ fst acts + loop ((ptrs++) . getPtrs) (mapM_ snd acts : final) + +-- | Auxiliary function used by all the primitives that create a +-- mutable variable. +addSignal :: (a -> IO a) -- ^ sampling function + -> (a -> IO ()) -- ^ aging function + -> IORef (Phase a) -- ^ the mutable variable behind the signal + -> IORef UpdatePool -- ^ the pool of update actions + -> IO (Signal a) +addSignal sample update ref pool = do + let upd = readIORef ref >>= \v -> case v of + Ready x -> update x + _ -> return () + + fin = readIORef ref >>= \v -> case v of + Updated x _ -> writeIORef ref $! Ready x + _ -> error "Signal not updated!" + + sig = S $ readIORef ref >>= \v -> case v of + Ready x -> sample x + Updated _ x -> return x + {-# NOINLINE sig #-} + -- NOINLINE to prevent sig from getting inlined into the + -- argument position of mkWeak. + + updateActions <- mkWeak sig (upd,fin) Nothing + modifyIORef pool (updateActions:) + return sig + +-- | The 'delay' combinator is the elementary building block for +-- adding state to the signal network by constructing delayed versions +-- of a signal that emit a given value at creation time and the +-- previous output of the signal afterwards (@--@ is undefined): +-- +-- > delay x0 s = <| <<x0 s0 s1 s2 s3 ...>> +-- > <<-- x0 s1 s2 s3 ...>> +-- > <<-- -- x0 s2 s3 ...>> +-- > <<-- -- -- x0 s3 ...>> +-- > ... +-- > |> +-- +-- It can be thought of as the following function (which should also +-- make it clear why the return value is 'SignalGen'): +-- +-- > delay x0 s t_start s_input t_sample +-- > | t_start == t_sample = x0 +-- > | t_start < t_sample = s (t_sample-1) +-- > | otherwise = error \"Premature sample!\" +-- +-- The way signal generators are extracted by 'generator' ensures that +-- the error can never happen. It is also clear that the behaviour of +-- 'delay' is not affected in any way by the global input. +-- +-- Example (requires the @DoRec@ extension): +-- +-- > do +-- > smp <- start $ do +-- > rec let fib'' = liftA2 (+) fib' fib +-- > fib' <- delay 1 fib'' +-- > fib <- delay 1 fib' +-- > return fib +-- > res <- replicateM 7 (smp undefined) +-- > print res +-- +-- Output: +-- +-- > [1,1,2,3,5,8,13] +delay :: a -- ^ initial output + -> Signal a -- ^ the signal to delay + -> SignalGen p (Signal a) +delay x0 (S s) = SG $ \pool _ -> do + ref <- newIORef (Ready x0) + + let update x = s >>= \x' -> x' `seq` writeIORef ref (Updated x' x) + + addSignal return update ref pool + +-- | A formal conversion from signals to signal generators, which +-- effectively allows for retrieving the current value of a previously +-- created signal within a generator. This includes both signals +-- defined in an external scope as well as those created earlier in +-- the same generator. It can be modelled by the following function: +-- +-- > snapshot s t_start s_input = s t_start +snapshot :: Signal a -> SignalGen p a +snapshot (S s) = SG $ \_ _ -> s + +-- | Auxiliary function. +memoise :: IORef (Phase a) -> a -> IO a +memoise ref x = writeIORef ref (Updated undefined x) >> return x + +-- | A reactive signal that takes the value to output from a signal +-- generator carried by its input with the sampling time provided as +-- the start time for the generated structure. It is possible to +-- create new signals in the monad, which is the key to defining +-- dynamic data-flow networks. +-- +-- > generator << <|x00 x01 x02 ...|> +-- > <|x10 x11 x12 ...|> +-- > <|x20 x21 x22 ...|> +-- > ... +-- > >> = <| <<x00 x11 x22 ...>> +-- > <<x00 x11 x22 ...>> +-- > <<x00 x11 x22 ...>> +-- > ... +-- > |> +-- +-- It can be thought of as the following function: +-- +-- > generator g t_start s_input t_sample = g t_sample t_sample s_input +-- +-- It has to live in the 'SignalGen' monad, because it needs to +-- maintain an internal state to be able to cache the current sample +-- for efficiency reasons. However, this state is not carried between +-- samples, therefore start time doesn't matter and can be ignored. +-- Also, even though it does not make use of the global input itself, +-- part of its job is to distribute it among the newly generated +-- signals. +-- +-- Refer to the longer example at the bottom of "FRP.Elerea.Simple" to +-- see how it can be used. +generator :: Signal (SignalGen p a) -- ^ the signal of generators to run + -> SignalGen p (Signal a) -- ^ the signal of generated structures +generator (S s) = SG $ \pool inp -> do + ref <- newIORef (Ready undefined) + + let sample = (s >>= \(SG g) -> g pool inp) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Memoising combinator. It can be used to cache results of +-- applicative combinators in case they are used in several places. +-- It is observationally equivalent to 'return' in the 'SignalGen' +-- monad. +-- +-- > memo s = <|s s s s ...|> +-- +-- For instance, if @s = f \<$\> s'@, then @f@ will be recalculated +-- once for each sampling of @s@. This can be avoided by writing @s +-- \<- memo (f \<$\> s')@ instead. However, 'memo' incurs a small +-- overhead, therefore it should not be used blindly. +-- +-- All the functions defined in this module return memoised signals. +-- Just like 'delay', it is independent of the global input. +memo :: Signal a -- ^ the signal to cache + -> SignalGen p (Signal a) -- ^ a signal observationally equivalent to the argument +memo (S s) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = s >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | A signal that is true exactly once: the first time the input +-- signal is true. Afterwards, it is constantly false, and it holds +-- no reference to the input signal. For instance (assuming the rest +-- of the input is constantly @False@): +-- +-- > till <<False False True True False True ...>> = +-- > <| <<False False True False False False False False False False ...>> +-- > << --- False True False False False False False False False ...>> +-- > << --- --- True False False False False False False False ...>> +-- > << --- --- --- True False False False False False False ...>> +-- > << --- --- --- --- False True False False False False ...>> +-- > << --- --- --- --- --- True False False False False ...>> +-- > << --- --- --- --- --- --- False False False False ...>> +-- > ... +-- > |> +-- +-- It is observationally equivalent to the following expression (which +-- would hold onto @s@ forever): +-- +-- > till s = do +-- > step <- transfer False (const (||)) s +-- > dstep <- delay False step +-- > memo (liftA2 (/=) step dstep) +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > accum <- stateful 0 (+) +-- > tick <- till ((>=10) <$> accum) +-- > return $ liftA2 (,) accum tick +-- > res <- forM [4,1,3,5,2,8,6] smp +-- > print res +-- +-- Output: +-- +-- > [(0,False),(4,False),(5,False),(8,False),(13,True),(15,False),(23,False)] +till :: Signal Bool -- ^ the boolean input signal + -> SignalGen p (Signal Bool) -- ^ a one-shot signal true only the first time the input is true +till (S s) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + rsmp <- mfix $ \rs -> newIORef $ do + x <- s + writeIORef ref (Updated undefined x) + when x $ writeIORef rs $ do + writeIORef ref (Updated undefined False) + return False + return x + + let sample = join (readIORef rsmp) + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | The common input signal that is fed through the function returned +-- by 'start', unless we are in an 'embed'ded generator. It is +-- equivalent to the following function: +-- +-- > input t_start s_input = s_input +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > sig <- input +-- > return (sig*2) +-- > res <- forM [4,1,3,5,2,8,6] smp +-- > print res +-- +-- Output: +-- +-- > [8,2,6,10,4,16,12] +input :: SignalGen p (Signal p) +input = SG $ const return + +-- | Embed a generator with an overridden input signal. It is +-- equivalent to the following function: +-- +-- > embed s g t_start s_input = g t_start s +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > sig <- input +-- > embed (sig*2) $ do +-- > sig <- input +-- > return (sig+1) +-- > res <- forM [4,1,3,5,2,8,6] smp +-- > print res +-- +-- Output: +-- +-- > [9,3,7,11,5,17,13] +embed :: Signal p' -> SignalGen p' a -> SignalGen p a +embed s (SG g) = SG $ \pool _ -> g pool s + +-- | A signal that can be directly fed through the sink function +-- returned. This can be used to attach the network to the outer +-- world. Note that this is optional, as all the input of the network +-- can be fed in through the global parameter, although that is not +-- really convenient for many signals. +-- +-- As for why this construct is unsafe, consult the explanation for +-- the equivalent in "FRP.Elerea.Simple". +unsafeExternal :: a -- ^ initial value + -> IO (Signal a, a -> IO ()) -- ^ the signal and an IO function to feed it +unsafeExternal x = do + ref <- newIORef x + return (S (readIORef ref), writeIORef ref) + +-- | A signal that can be directly fed through the sink function +-- returned. This can be used to attach the network to the outer +-- world. The signal always yields the value last written to the +-- sink at the start of the superstep. In other words, if the sink +-- is written less frequently than the network sampled, the output +-- remains the same during several samples. If values are pushed +-- in the sink more frequently, only the last one before sampling +-- is visible on the output. +external :: a -- ^ initial value + -> IO (SignalGen p (Signal a), a -> IO ()) -- ^ the generator to create the signal and an IO function to feed it +external x = do + ref <- newIORef x + return (SG $ \pool _ -> do + memoRef <- newIORef (Updated undefined x) + let sample = readIORef ref >>= memoise memoRef + addSignal (const sample) (const (() <$ sample)) memoRef pool + ,writeIORef ref + ) + +-- | An event-like signal that can be fed through the sink function +-- returned. The signal carries a list of values fed in since the +-- last sampling, i.e. it is constantly [] if the sink is never +-- invoked. The order of elements is reversed, so the last value +-- passed to the sink is the head of the list. +externalMulti :: IO (SignalGen p (Signal [a]), a -> IO ()) -- ^ a generator for the event signal and the associated sink +externalMulti = do + var <- newMVar [] + return (SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + let sample = modifyMVar var $ \list -> memoise ref list >> return ([], list) + addSignal (const sample) (const (() <$ sample)) ref pool + ,\val -> do vals <- takeMVar var + putMVar var (val:vals) + ) + +-- | A direct stateful transformation of the input. The initial state +-- is the first output, and every following output is calculated from +-- the previous one and the value of the global parameter (which might +-- have been overridden by 'embed'). +-- +-- Example: +-- +-- > do +-- > smp <- start (stateful "" (:)) +-- > res <- forM "olleh~" smp +-- > print res +-- +-- Output: +-- +-- > ["","o","lo","llo","ello","hello"] +stateful :: a -- ^ initial state + -> (p -> a -> a) -- ^ state transformation + -> SignalGen p (Signal a) +stateful x0 f = mfix $ \sig -> input >>= \i -> delay x0 (f <$> i <*> sig) + +-- | A stateful transfer function. The current input affects the +-- current output, i.e. the initial state given in the first argument +-- is considered to appear before the first output, and can never be +-- observed. Every output is derived from the current value of the +-- input signal, the global parameter (which might have been +-- overridden by 'embed') and the previous output. It is equivalent +-- to the following expression: +-- +-- Example (assuming a delta time is passed to the sampling function +-- in each step): +-- +-- > integral x0 s = transfer x0 (\dt v x -> x+dt*v) +-- +-- Example for using the above: +-- +-- > do +-- > smp <- start (integral 3 (pure 2)) +-- > res <- replicateM 7 (smp 0.1) +-- > print res +-- +-- Output: +-- +-- > [3.2,3.4,3.6,3.8,4.0,4.2,4.4] +transfer :: a -- ^ initial internal state + -> (p -> t -> a -> a) -- ^ state updater function + -> Signal t -- ^ input signal + -> SignalGen p (Signal a) +transfer x0 f s = mfix $ \sig -> do + inp <- input + sig' <- delay x0 sig + memo (liftA3 f inp s sig') + +-- | A variation of 'transfer' with two input signals. +transfer2 :: a -- ^ initial internal state + -> (p -> t1 -> t2 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> SignalGen p (Signal a) +transfer2 x0 f s1 s2 = mfix $ \sig -> do + inp <- input + sig' <- delay x0 sig + memo (liftM4 f inp s1 s2 sig') + +-- | A variation of 'transfer' with three input signals. +transfer3 :: a -- ^ initial internal state + -> (p -> t1 -> t2 -> t3 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> Signal t3 -- ^ input signal 3 + -> SignalGen p (Signal a) +transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do + inp <- input + sig' <- delay x0 sig + memo (liftM5 f inp s1 s2 s3 sig') + +-- | A variation of 'transfer' with four input signals. +transfer4 :: a -- ^ initial internal state + -> (p -> t1 -> t2 -> t3 -> t4 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> Signal t3 -- ^ input signal 3 + -> Signal t4 -- ^ input signal 4 + -> SignalGen p (Signal a) +transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do + inp <- input + sig' <- delay x0 sig + memo (liftM5 f inp s1 s2 s3 s4 `ap` sig') + +{- $effectful + +The following combinators are primarily aimed at library implementors +who wish build abstractions to effectful libraries on top of Elerea. + +-} + +-- | An IO action executed in the 'SignalGen' monad. Can be used as +-- `liftIO`. +execute :: IO a -> SignalGen p a +execute act = SG $ \_ _ -> act + +-- | A signal that executes a given IO action once at every sampling. +-- +-- In essence, this combinator provides cooperative multitasking +-- capabilities, and its primary purpose is to assist library writers +-- in wrapping effectful APIs as conceptually pure signals. If there +-- are several effectful signals in the system, their order of +-- execution is undefined and should not be relied on. +-- +-- Example: +-- +-- > do +-- > act <- start $ do +-- > ref <- execute $ newIORef 0 +-- > let accum n = do +-- > x <- readIORef ref +-- > putStrLn $ "Accumulator: " ++ show x +-- > writeIORef ref $! x+n +-- > return () +-- > effectful1 accum =<< input +-- > forM_ [4,9,2,1,5] act +-- +-- Output: +-- +-- > Accumulator: 0 +-- > Accumulator: 4 +-- > Accumulator: 13 +-- > Accumulator: 15 +-- > Accumulator: 16 +-- +-- Another example (requires mersenne-random): +-- +-- > do +-- > smp <- start $ effectful randomIO :: IO (IO Double) +-- > res <- replicateM 5 smp +-- > print res +-- +-- Output: +-- +-- > [0.12067753390401374,0.8658877349182655,0.7159264443196786,0.1756941896012891,0.9513646060896676] +effectful :: IO a -- ^ the action to be executed repeatedly + -> SignalGen p (Signal a) +effectful act = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = act >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | A signal that executes a parametric IO action once at every +-- sampling. The parameter is supplied by another signal at every +-- sampling step. +effectful1 :: (t -> IO a) -- ^ the action to be executed repeatedly + -> Signal t -- ^ parameter signal + -> SignalGen p (Signal a) +effectful1 act (S s) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = s >>= act >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with two parameter signals. +effectful2 :: (t1 -> t2 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> SignalGen p (Signal a) +effectful2 act (S s1) (S s2) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM2 act s1 s2) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with three parameter signals. +effectful3 :: (t1 -> t2 -> t3 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> Signal t3 -- ^ parameter signal 3 + -> SignalGen p (Signal a) +effectful3 act (S s1) (S s2) (S s3) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM3 act s1 s2 s3) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with four parameter signals. +effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> Signal t3 -- ^ parameter signal 3 + -> Signal t4 -- ^ parameter signal 4 + -> SignalGen p (Signal a) +effectful4 act (S s1) (S s2) (S s3) (S s4) = SG $ \pool _ -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM4 act s1 s2 s3 s4) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | The @Show@ instance is only defined for the sake of 'Num'... +instance Show (Signal a) where + showsPrec _ _ s = "<SIGNAL>" ++ s + +-- | Equality test is impossible. +instance Eq (Signal a) where + _ == _ = False + +-- | Error message for unimplemented instance functions. +unimp :: String -> a +unimp = error . ("Signal: "++) + +instance Ord t => Ord (Signal t) where + compare = unimp "compare" + min = liftA2 min + max = liftA2 max + +instance Enum t => Enum (Signal t) where + succ = fmap succ + pred = fmap pred + toEnum = pure . toEnum + fromEnum = unimp "fromEnum" + enumFrom = unimp "enumFrom" + enumFromThen = unimp "enumFromThen" + enumFromTo = unimp "enumFromTo" + enumFromThenTo = unimp "enumFromThenTo" + +instance Bounded t => Bounded (Signal t) where + minBound = pure minBound + maxBound = pure maxBound + +instance Num t => Num (Signal t) where + (+) = liftA2 (+) + (-) = liftA2 (-) + (*) = liftA2 (*) + signum = fmap signum + abs = fmap abs + negate = fmap negate + fromInteger = pure . fromInteger + +instance Real t => Real (Signal t) where + toRational = unimp "toRational" + +instance Integral t => Integral (Signal t) where + quot = liftA2 quot + rem = liftA2 rem + div = liftA2 div + mod = liftA2 mod + quotRem a b = (fst <$> qrab,snd <$> qrab) + where qrab = quotRem <$> a <*> b + divMod a b = (fst <$> dmab,snd <$> dmab) + where dmab = divMod <$> a <*> b + toInteger = unimp "toInteger" + +instance Fractional t => Fractional (Signal t) where + (/) = liftA2 (/) + recip = fmap recip + fromRational = pure . fromRational + +instance Floating t => Floating (Signal t) where + pi = pure pi + exp = fmap exp + sqrt = fmap sqrt + log = fmap log + (**) = liftA2 (**) + logBase = liftA2 logBase + sin = fmap sin + tan = fmap tan + cos = fmap cos + asin = fmap asin + atan = fmap atan + acos = fmap acos + sinh = fmap sinh + tanh = fmap tanh + cosh = fmap cosh + asinh = fmap asinh + atanh = fmap atanh + acosh = fmap acosh
FRP/Elerea/Simple.hs view
@@ -1,823 +1,866 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses #-}--{-|--This module provides leak-free and referentially transparent-higher-order discrete signals.---}--module FRP.Elerea.Simple- (- -- * The signal abstraction- Signal- , SignalGen- -- * Embedding into I/O- , start- , external- , externalMulti- -- * Basic building blocks- , delay- , snapshot- , generator- , memo- , until- -- * Derived combinators- , stateful- , transfer- , transfer2- , transfer3- , transfer4- -- * Signals with side effects- -- $effectful- , execute- , effectful- , effectful1- , effectful2- , effectful3- , effectful4- -- * A longer example- -- $example- ) where--import Control.Applicative-import Control.Concurrent.MVar-import Control.Monad-import Control.Monad.Base-import Control.Monad.Fix-import Control.Monad.IO.Class-import Data.IORef-import Data.Maybe-import Prelude hiding (until)-import System.Mem.Weak---- | A signal represents a value changing over time. It can be--- thought of as a function of type @Nat -> a@, where the argument is--- the sampling time, and the 'Monad' instance agrees with the--- intuition (bind corresponds to extracting the current sample).--- Signals and the values they carry are denoted the following way in--- the documentation:------ > s = <<s0 s1 s2 ...>>------ This says that @s@ is a signal that reads @s0@ during the first--- sampling, @s1@ during the second and so on. You can also think of--- @s@ as the following function:------ > s t_sample = [s0,s1,s2,...] !! t_sample------ Signals are constrained to be sampled sequentially, there is no--- random access. The only way to observe their output is through--- 'start'.-newtype Signal a = S (IO a) deriving (Functor, Applicative, Monad)---- | A dynamic set of actions to update a network without breaking--- consistency.-type UpdatePool = [Weak (IO (), IO ())]---- | A signal generator is the only source of stateful signals. It--- can be thought of as a function of type @Nat -> a@, where the--- result is an arbitrary data structure that can potentially contain--- new signals, and the argument is the creation time of these new--- signals. It exposes the 'MonadFix' interface, which makes it--- possible to define signals in terms of each other. The denotation--- of signal generators happens to be the same as that of signals, but--- this partly accidental (it does not hold in the other variants), so--- we will use a separate notation for generators:------ > g = <|g0 g1 g2 ...|>------ Just like signals, generators behave as functions of time:------ > g t_start = [g0,g1,g2,...] !! t_start------ The conceptual difference between the two notions is that signals--- are passed a sampling time, while generators expect a start time--- that will be the creation time of all the freshly generated--- signals in the resulting structure.-newtype SignalGen a = SG { unSG :: IORef UpdatePool -> IO a }---- | The phases every signal goes through during a superstep.-data Phase a = Ready a | Updated a a--instance Functor SignalGen where- fmap = liftM--instance Applicative SignalGen where- pure = return- (<*>) = ap--instance Monad SignalGen where- return x = SG $ \_ -> return x- SG g >>= f = SG $ \p -> g p >>= \x -> unSG (f x) p--instance MonadFix SignalGen where- mfix f = SG $ \p -> mfix $ \x -> unSG (f x) p--instance MonadIO SignalGen where- liftIO = execute--instance MonadBase SignalGen SignalGen where- liftBase = id---- | Embedding a signal into an 'IO' environment. Repeated calls to--- the computation returned cause the whole network to be updated, and--- the current sample of the top-level signal is produced as a--- result. This is the only way to extract a signal generator outside--- the network, and it is equivalent to passing zero to the function--- representing the generator. In general:------ > replicateM n =<< start <|<<x0 x1 x2 x3 ...>> ...|> == take n [x0,x1,x2,x3,...]------ Example:------ > do--- > smp <- start (stateful 3 (+2))--- > res <- replicateM 5 smp--- > print res------ Output:------ > [3,5,7,9,11]-start :: SignalGen (Signal a) -- ^ the generator of the top-level signal- -> IO (IO a) -- ^ the computation to sample the signal-start (SG gen) = do- pool <- newIORef []- S sample <- gen pool- return $ do- res <- sample- superstep pool- return res---- | Performing the two-phase superstep.-superstep :: IORef UpdatePool -> IO ()-superstep pool = loop id []- where- deref ptr = (fmap.fmap) ((,) ptr) (deRefWeak ptr)- loop getPtrs final = do- (ptrs,acts) <- unzip.catMaybes <$> (mapM deref =<< readIORef pool)- case acts of- [] -> do- sequence_ final- writeIORef pool (getPtrs [])- _ -> do- writeIORef pool []- mapM_ fst acts- loop ((ptrs++) . getPtrs) (mapM_ snd acts : final)---- | Auxiliary function used by all the primitives that create a--- mutable variable.-addSignal :: (a -> IO a) -- ^ sampling function- -> (a -> IO ()) -- ^ aging function- -> IORef (Phase a) -- ^ the mutable variable behind the signal- -> IORef UpdatePool -- ^ the pool of update actions- -> IO (Signal a) -- ^ the signal created-addSignal sample update ref pool = do- let upd = readIORef ref >>= \v -> case v of- Ready x -> update x- _ -> return ()-- fin = readIORef ref >>= \v -> case v of- Updated x _ -> writeIORef ref $! Ready x- _ -> error "Signal not updated!"-- sig = S $ readIORef ref >>= \v -> case v of- Ready x -> sample x- Updated _ x -> return x- {-# NOINLINE sig #-}- -- NOINLINE to prevent sig from getting inlined into the- -- argument position of mkWeak.-- updateActions <- mkWeak sig (upd,fin) Nothing- modifyIORef pool (updateActions:)- return sig---- | The 'delay' combinator is the elementary building block for--- adding state to the signal network by constructing delayed versions--- of a signal that emit a given value at creation time and the--- previous output of the signal afterwards (@--@ is undefined):------ > delay x0 s = <| <<x0 s0 s1 s2 s3 ...>>--- > <<-- x0 s1 s2 s3 ...>>--- > <<-- -- x0 s2 s3 ...>>--- > <<-- -- -- x0 s3 ...>>--- > ...--- > |>------ It can be thought of as the following function (which should also--- make it clear why the return value is 'SignalGen'):------ > delay x0 s t_start t_sample--- > | t_start == t_sample = x0--- > | t_start < t_sample = s (t_sample-1)--- > | otherwise = error \"Premature sample!\"------ The way signal generators are extracted by 'generator' ensures that--- the error can never happen.------ Example (requires the @DoRec@ extension):------ > do--- > smp <- start $ do--- > rec let fib'' = liftA2 (+) fib' fib--- > fib' <- delay 1 fib''--- > fib <- delay 1 fib'--- > return fib--- > res <- replicateM 7 smp--- > print res------ Output:------ > [1,1,2,3,5,8,13]-delay :: a -- ^ initial output at creation time- -> Signal a -- ^ the signal to delay- -> SignalGen (Signal a) -- ^ the delayed signal-delay x0 (S s) = SG $ \pool -> do- ref <- newIORef (Ready x0)-- let update x = s >>= \x' -> x' `seq` writeIORef ref (Updated x' x)-- addSignal return update ref pool---- | A formal conversion from signals to signal generators, which--- effectively allows for retrieving the current value of a previously--- created signal within a generator. This includes both signals--- defined in an external scope as well as those created earlier in--- the same generator. In the model, it corresponds to the identity--- function.-snapshot :: Signal a -> SignalGen a-snapshot (S s) = SG $ \_ -> s---- | Auxiliary function.-memoise :: IORef (Phase a) -> a -> IO a-memoise ref x = writeIORef ref (Updated undefined x) >> return x---- | A reactive signal that takes the value to output from a signal--- generator carried by its input with the sampling time provided as--- the start time for the generated structure. It is possible to--- create new signals in the monad, which is the key to defining--- dynamic data-flow networks.------ > generator << <|x00 x01 x02 ...|>--- > <|x10 x11 x12 ...|>--- > <|x20 x21 x22 ...|>--- > ...--- > >> = <| <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > <<x00 x11 x22 ...>>--- > ...--- > |>------ It can be thought of as the following function:------ > generator g t_start t_sample = g t_sample t_sample------ It has to live in the 'SignalGen' monad, because it needs to--- maintain an internal state to be able to cache the current sample--- for efficiency reasons. However, this state is not carried between--- samples, therefore start time doesn't matter and can be ignored.------ Refer to the longer example at the bottom to see how it can be--- used.-generator :: Signal (SignalGen a) -- ^ the signal of generators to run- -> SignalGen (Signal a) -- ^ the signal of generated structures-generator (S s) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = (s >>= \(SG g) -> g pool) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Memoising combinator. It can be used to cache results of--- applicative combinators in case they are used in several places.--- It is observationally equivalent to 'return' in the 'SignalGen'--- monad.------ > memo s = <|s s s s ...|>------ For instance, if @s = f \<$\> s'@, then @f@ will be recalculated--- once for each sampling of @s@. This can be avoided by writing @s--- \<- memo (f \<$\> s')@ instead. However, 'memo' incurs a small--- overhead, therefore it should not be used blindly.------ All the functions defined in this module return memoised signals.-memo :: Signal a -- ^ the signal to cache- -> SignalGen (Signal a) -- ^ a signal observationally equivalent to the argument-memo (S s) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that is true exactly once: the first time the input--- signal is true. Afterwards, it is constantly false, and it holds--- no reference to the input signal. For instance (assuming the rest--- of the input is constantly @False@):------ > until <<False False True True False True ...>> =--- > <| <<False False True False False False False False False False ...>>--- > << --- False True False False False False False False False ...>>--- > << --- --- True False False False False False False False ...>>--- > << --- --- --- True False False False False False False ...>>--- > << --- --- --- --- False True False False False False ...>>--- > << --- --- --- --- --- True False False False False ...>>--- > << --- --- --- --- --- --- False False False False ...>>--- > ...--- > |>------ It is observationally equivalent to the following expression (which--- would hold onto @s@ forever):------ > until s = do--- > step <- transfer False (||) s--- > dstep <- delay False step--- > memo (liftA2 (/=) step dstep)------ Example:------ > do--- > smp <- start $ do--- > cnt <- stateful 0 (+1)--- > tick <- until ((>=3) <$> cnt)--- > return $ liftA2 (,) cnt tick--- > res <- replicateM 6 smp--- > print res------ Output:------ > [(0,False),(1,False),(2,False),(3,True),(4,False),(5,False)]-until :: Signal Bool -- ^ the boolean input signal- -> SignalGen (Signal Bool) -- ^ a one-shot signal true only the first time the input is true-until (S s) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- rsmp <- mfix $ \rs -> newIORef $ do- x <- s- writeIORef ref (Updated undefined x)- when x $ writeIORef rs $ do- writeIORef ref (Updated undefined False)- return False- return x-- let sample = join (readIORef rsmp)-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that can be directly fed through the sink function--- returned. This can be used to attach the network to the outer--- world. The signal always yields the value last written to the--- sink. In other words, if the sink is written less frequently than--- the network sampled, the output remains the same during several--- samples. If values are pushed in the sink more frequently, only--- the last one before sampling is visible on the output.------ Example:------ > do--- > (sig,snk) <- external 4--- > smp <- start (return sig)--- > r1 <- smp--- > r2 <- smp--- > snk 7--- > r3 <- smp--- > snk 9--- > snk 2--- > r4 <- smp--- > print [r1,r2,r3,r4]------ Output:------ > [4,4,7,2]-external :: a -- ^ initial value- -> IO (Signal a, a -> IO ()) -- ^ the signal and an IO function to feed it-external x = do- ref <- newIORef x- return (S (readIORef ref), writeIORef ref)---- | An event-like signal that can be fed through the sink function--- returned. The signal carries a list of values fed in since the--- last sampling, i.e. it is constantly @[]@ if the sink is never--- invoked. The order of elements is reversed, so the last value--- passed to the sink is the head of the list. Note that unlike--- 'external' this function only returns a generator to be used within--- the expression constructing the top-level stream, and this--- generator can only be used once.------ Example:------ > do--- > (gen,snk) <- externalMulti--- > smp <- start gen--- > r1 <- smp--- > snk 7--- > r2 <- smp--- > r3 <- smp--- > snk 9--- > snk 2--- > r4 <- smp--- > print [r1,r2,r3,r4]------ Output:------ > [[],[7],[],[2,9]]-externalMulti :: IO (SignalGen (Signal [a]), a -> IO ()) -- ^ a generator for the event signal and the associated sink-externalMulti = do- var <- newMVar []- return (SG $ \pool -> do- ref <- newIORef (Ready undefined)- let sample = modifyMVar var $ \list -> memoise ref list >> return ([], list)- addSignal (const sample) (const (() <$ sample)) ref pool- ,\val -> do vals <- takeMVar var- putMVar var (val:vals)- )---- | A pure stateful signal. The initial state is the first output,--- and every subsequent state is derived from the preceding one by--- applying a pure transformation.------ Example:------ > do--- > smp <- start (stateful "x" ('x':))--- > res <- replicateM 5 smp--- > print res------ Output:------ > ["x","xx","xxx","xxxx","xxxxx"]-stateful :: a -- ^ initial state- -> (a -> a) -- ^ state transformation- -> SignalGen (Signal a)-stateful x0 f = mfix $ \sig -> delay x0 (f <$> sig)---- | A stateful transfer function. The current input affects the--- current output, i.e. the initial state given in the first argument--- is considered to appear before the first output, and can never be--- observed, and subsequent states are determined by combining the--- preceding state with the current output of the input signal using--- the function supplied.------ Example:------ > do--- > smp <- start $ do--- > cnt <- stateful 1 (+1)--- > transfer 10 (+) cnt--- > res <- replicateM 5 smp--- > print res------ Output:------ > [11,13,16,20,25]-transfer :: a -- ^ initial internal state- -> (t -> a -> a) -- ^ state updater function- -> Signal t -- ^ input signal- -> SignalGen (Signal a)-transfer x0 f s = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA2 f s sig')---- | A variation of 'transfer' with two input signals.-transfer2 :: a -- ^ initial internal state- -> (t1 -> t2 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> SignalGen (Signal a)-transfer2 x0 f s1 s2 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA3 f s1 s2 sig')---- | A variation of 'transfer' with three input signals.-transfer3 :: a -- ^ initial internal state- -> (t1 -> t2 -> t3 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> SignalGen (Signal a)-transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM4 f s1 s2 s3 sig')---- | A variation of 'transfer' with four input signals.-transfer4 :: a -- ^ initial internal state- -> (t1 -> t2 -> t3 -> t4 -> a -> a) -- ^ state updater function- -> Signal t1 -- ^ input signal 1- -> Signal t2 -- ^ input signal 2- -> Signal t3 -- ^ input signal 3- -> Signal t4 -- ^ input signal 4- -> SignalGen (Signal a)-transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM5 f s1 s2 s3 s4 sig')--{- $effectful--The following combinators are primarily aimed at library implementors-who wish build abstractions to effectful libraries on top of Elerea.---}---- | An IO action executed in the 'SignalGen' monad. Can be used as--- `liftIO`.-execute :: IO a -> SignalGen a-execute act = SG $ \_ -> act---- | A signal that executes a given IO action once at every sampling.------ In essence, this combinator provides cooperative multitasking--- capabilities, and its primary purpose is to assist library writers--- in wrapping effectful APIs as conceptually pure signals. If there--- are several effectful signals in the system, their order of--- execution is undefined and should not be relied on.------ Example:------ > do--- > smp <- start $ do--- > ref <- execute $ newIORef 0--- > effectful $ do--- > x <- readIORef ref--- > putStrLn $ "Count: " ++ show x--- > writeIORef ref $! x+1--- > return ()--- > replicateM_ 5 smp------ Output:------ > Count: 0--- > Count: 1--- > Count: 2--- > Count: 3--- > Count: 4------ Another example (requires mersenne-random):------ > do--- > smp <- start $ effectful randomIO :: IO (IO Double)--- > res <- replicateM 5 smp--- > print res------ Output:------ > [0.12067753390401374,0.8658877349182655,0.7159264443196786,0.1756941896012891,0.9513646060896676]-effectful :: IO a -- ^ the action to be executed repeatedly- -> SignalGen (Signal a)-effectful act = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | A signal that executes a parametric IO action once at every--- sampling. The parameter is supplied by another signal at every--- sampling step.-effectful1 :: (t -> IO a) -- ^ the action to be executed repeatedly- -> Signal t -- ^ parameter signal- -> SignalGen (Signal a)-effectful1 act (S s) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = s >>= act >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with two parameter signals.-effectful2 :: (t1 -> t2 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> SignalGen (Signal a)-effectful2 act (S s1) (S s2) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM2 act s1 s2) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with three parameter signals.-effectful3 :: (t1 -> t2 -> t3 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> SignalGen (Signal a)-effectful3 act (S s1) (S s2) (S s3) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM3 act s1 s2 s3) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | Like 'effectful1', but with four parameter signals.-effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -- ^ the action to be executed repeatedly- -> Signal t1 -- ^ parameter signal 1- -> Signal t2 -- ^ parameter signal 2- -> Signal t3 -- ^ parameter signal 3- -> Signal t4 -- ^ parameter signal 4- -> SignalGen (Signal a)-effectful4 act (S s1) (S s2) (S s3) (S s4) = SG $ \pool -> do- ref <- newIORef (Ready undefined)-- let sample = join (liftM4 act s1 s2 s3 s4) >>= memoise ref-- addSignal (const sample) (const (() <$ sample)) ref pool---- | The Show instance is only defined for the sake of Num...-instance Show (Signal a) where- showsPrec _ _ s = "<SIGNAL>" ++ s---- | Equality test is impossible.-instance Eq (Signal a) where- _ == _ = False---- | Error message for unimplemented instance functions.-unimp :: String -> a-unimp = error . ("Signal: "++)--instance Ord t => Ord (Signal t) where- compare = unimp "compare"- min = liftA2 min- max = liftA2 max--instance Enum t => Enum (Signal t) where- succ = fmap succ- pred = fmap pred- toEnum = pure . toEnum- fromEnum = unimp "fromEnum"- enumFrom = unimp "enumFrom"- enumFromThen = unimp "enumFromThen"- enumFromTo = unimp "enumFromTo"- enumFromThenTo = unimp "enumFromThenTo"--instance Bounded t => Bounded (Signal t) where- minBound = pure minBound- maxBound = pure maxBound--instance Num t => Num (Signal t) where- (+) = liftA2 (+)- (-) = liftA2 (-)- (*) = liftA2 (*)- signum = fmap signum- abs = fmap abs- negate = fmap negate- fromInteger = pure . fromInteger--instance Real t => Real (Signal t) where- toRational = unimp "toRational"--instance Integral t => Integral (Signal t) where- quot = liftA2 quot- rem = liftA2 rem- div = liftA2 div- mod = liftA2 mod- quotRem a b = (fst <$> qrab,snd <$> qrab)- where qrab = quotRem <$> a <*> b- divMod a b = (fst <$> dmab,snd <$> dmab)- where dmab = divMod <$> a <*> b- toInteger = unimp "toInteger"--instance Fractional t => Fractional (Signal t) where- (/) = liftA2 (/)- recip = fmap recip- fromRational = pure . fromRational--instance Floating t => Floating (Signal t) where- pi = pure pi- exp = fmap exp- sqrt = fmap sqrt- log = fmap log- (**) = liftA2 (**)- logBase = liftA2 logBase- sin = fmap sin- tan = fmap tan- cos = fmap cos- asin = fmap asin- atan = fmap atan- acos = fmap acos- sinh = fmap sinh- tanh = fmap tanh- cosh = fmap cosh- asinh = fmap asinh- atanh = fmap atanh- acosh = fmap acosh--{- $example--For a not entirely trivial example, let's create a dynamic collection-of countdown timers, where each expired timer is removed from the-collection. First of all, we'll need a simple tester function:--@- sigtest gen = 'replicateM' 15 '=<<' 'start' gen-@--We can try it with a trivial example:--@- \> sigtest $ 'stateful' 2 (+3)- [2,5,8,11,14,17,20,23,26,29,32,35,38,41,44,47]-@--Our first definition will be a signal representing a simple named-timer:--@- countdown :: String -\> Int -\> SignalGen (Signal (String,Maybe Int))- countdown name t = do- let tick prev = do { t \<- prev ; 'guard' (t \> 0) ; 'return' (t-1) }- timer \<- 'stateful' (Just t) tick- 'return' ((,) name '<$>' timer)-@--Let's see if it works:--@- \> sigtest $ countdown \"foo\" 4- [(\"foo\",Just 4),(\"foo\",Just 3),(\"foo\",Just 2),(\"foo\",Just 1),(\"foo\",Just 0),- (\"foo\",Nothing),(\"foo\",Nothing),(\"foo\",Nothing),...]-@--Next, we will define a timer source that takes a list of timer names,-starting values and start times and creates a signal that delivers the-list of new timers at every point:--@- timerSource :: [(String, Int, Int)] -\> SignalGen (Signal [Signal (String, Maybe Int)])- timerSource ts = do- let gen t = 'mapM' ('uncurry' countdown) newTimers- where newTimers = [(n,v) | (n,v,st) \<- ts, st == t]- cnt \<- 'stateful' 0 (+1)- 'generator' (gen '<$>' cnt)-@--Now we need to encapsulate the timer source signal in another signal-expression that takes care of maintaining the list of live timers.-Since working with dynamic collections is a recurring task, let's-define a generic combinator that maintains a dynamic list of signals-given a source and a test that tells from the output of each signal-whether it should be kept. We can use @mdo@ expressions (a variant of-@do@ expressions allowing forward references) as syntactic sugar for-'mfix' to make life easier:--@- collection :: Signal [Signal a] -\> (a -\> Bool) -\> SignalGen (Signal [a])- collection source isAlive = mdo- sig \<- 'delay' [] ('map' 'snd' '<$>' collWithVals')- coll \<- 'memo' ('liftA2' (++) source sig)- let collWithVals = 'zip' '<$>' ('sequence' '=<<' coll) '<*>' coll- collWithVals' \<- 'memo' ('filter' (isAlive . 'fst') '<$>' collWithVals)- 'return' $ 'map' 'fst' '<$>' collWithVals'-@--We need recursion to define the @coll@ signal as a delayed version of-its continuation, which does not contain signals that need to be-removed in the current sample. At every point of time the running-collection is concatenated with the source. We define @collWithVals@,-which simply pairs up every signal with its current output. The-output is obtained by extracting the current value of the signal-container and sampling each element with 'sequence'. We can then-derive @collWithVals'@, which contains only the signals that must be-kept for the next round along with their output. Both @coll@ and-@collWithVals'@ have to be memoised, because they are used more than-once (the program would work without that, but it would recalculate-both signals each time they are used). By throwing out the respective-parts, we can get both the final output and the collection for the-next step (@coll'@).--Now we can easily finish the original task:--@- timers :: [(String, Int, Int)] -\> SignalGen (Signal [(String, Int)])- timers timerData = do- src \<- timerSource timerData- getOutput '<$>' collection src ('isJust' . 'snd')- where getOutput = 'fmap' ('map' (\\(name,Just val) -> (name,val)))-@--As a test, we can start four timers: /a/ at t=0 with value 3, /b/ and-/c/ at t=1 with values 5 and 3, and /d/ at t=3 with value 4:--@- \> sigtest $ timers [(\"a\",3,0),(\"b\",5,1),(\"c\",3,1),(\"d\",4,3)]- [[(\"a\",3)],[(\"b\",5),(\"c\",3),(\"a\",2)],[(\"b\",4),(\"c\",2),(\"a\",1)],- [(\"d\",4),(\"b\",3),(\"c\",1),(\"a\",0)],[(\"d\",3),(\"b\",2),(\"c\",0)],- [(\"d\",2),(\"b\",1)],[(\"d\",1),(\"b\",0)],[(\"d\",0)],[],[],[],[],[],[],[]]-@--If the noise of the applicative lifting operators feels annoying, she-(<http://personal.cis.strath.ac.uk/~conor/pub/she/>) comes to the-save. Among other features it provides idiom brackets, which can-substitute the explicit lifting. For instance, it allows us to define-@collection@ this way:--@- collection :: Stream [Stream a] -> (a -> Bool) -> StreamGen (Stream [a])- collection source isAlive = mdo- sig \<- 'delay' [] (|'map' ~'snd' collWithVals'|)- coll \<- 'memo' (|source ++ sig|)- collWithVals' \<- 'memo' (|'filter' ~(isAlive . 'fst') (|'zip' ('sequence' '=<<' coll) coll|)|)- 'return' (|'map' ~'fst' collWithVals'|)-@---}+{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} + +{-| + +This module provides leak-free and referentially transparent +higher-order discrete signals. + +-} + +module FRP.Elerea.Simple + ( + -- * The signal abstraction + Signal + , SignalGen + -- * Embedding into I/O + , start + , external + , externalMulti + , unsafeExternal + -- * Basic building blocks + , delay + , snapshot + , generator + , memo + , till + -- * Derived combinators + , stateful + , transfer + , transfer2 + , transfer3 + , transfer4 + -- * Signals with side effects + -- $effectful + , execute + , effectful + , effectful1 + , effectful2 + , effectful3 + , effectful4 + -- * A longer example + -- $example + ) where + +import Control.Applicative +import Control.Concurrent.MVar +import Control.Monad +import Control.Monad.Base +import Control.Monad.Fix +import Control.Monad.IO.Class +import Data.IORef +import Data.Maybe +import System.Mem.Weak + +-- | A signal represents a value changing over time. It can be +-- thought of as a function of type @Nat -> a@, where the argument is +-- the sampling time, and the 'Monad' instance agrees with the +-- intuition (bind corresponds to extracting the current sample). +-- Signals and the values they carry are denoted the following way in +-- the documentation: +-- +-- > s = <<s0 s1 s2 ...>> +-- +-- This says that @s@ is a signal that reads @s0@ during the first +-- sampling, @s1@ during the second and so on. You can also think of +-- @s@ as the following function: +-- +-- > s t_sample = [s0,s1,s2,...] !! t_sample +-- +-- Signals are constrained to be sampled sequentially, there is no +-- random access. The only way to observe their output is through +-- 'start'. +newtype Signal a = S (IO a) deriving (Functor, Applicative, Monad) + +-- | A dynamic set of actions to update a network without breaking +-- consistency. +type UpdatePool = [Weak (IO (), IO ())] + +-- | A signal generator is the only source of stateful signals. It +-- can be thought of as a function of type @Nat -> a@, where the +-- result is an arbitrary data structure that can potentially contain +-- new signals, and the argument is the creation time of these new +-- signals. It exposes the 'MonadFix' interface, which makes it +-- possible to define signals in terms of each other. The denotation +-- of signal generators happens to be the same as that of signals, but +-- this partly accidental (it does not hold in the other variants), so +-- we will use a separate notation for generators: +-- +-- > g = <|g0 g1 g2 ...|> +-- +-- Just like signals, generators behave as functions of time: +-- +-- > g t_start = [g0,g1,g2,...] !! t_start +-- +-- The conceptual difference between the two notions is that signals +-- are passed a sampling time, while generators expect a start time +-- that will be the creation time of all the freshly generated +-- signals in the resulting structure. +newtype SignalGen a = SG { unSG :: IORef UpdatePool -> IO a } + +-- | The phases every signal goes through during a superstep. +data Phase a = Ready a | Updated a a + +instance Functor SignalGen where + fmap = liftM + +instance Applicative SignalGen where + pure = return + (<*>) = ap + +instance Monad SignalGen where + return x = SG $ \_ -> return x + SG g >>= f = SG $ \p -> g p >>= \x -> unSG (f x) p + +instance MonadFix SignalGen where + mfix f = SG $ \p -> mfix $ \x -> unSG (f x) p + +instance MonadIO SignalGen where + liftIO = execute + +instance MonadBase SignalGen SignalGen where + liftBase = id + +-- | Embedding a signal into an 'IO' environment. Repeated calls to +-- the computation returned cause the whole network to be updated, and +-- the current sample of the top-level signal is produced as a +-- result. This is the only way to extract a signal generator outside +-- the network, and it is equivalent to passing zero to the function +-- representing the generator. In general: +-- +-- > replicateM n =<< start <|<<x0 x1 x2 x3 ...>> ...|> == take n [x0,x1,x2,x3,...] +-- +-- Example: +-- +-- > do +-- > smp <- start (stateful 3 (+2)) +-- > res <- replicateM 5 smp +-- > print res +-- +-- Output: +-- +-- > [3,5,7,9,11] +start :: SignalGen (Signal a) -- ^ the generator of the top-level signal + -> IO (IO a) -- ^ the computation to sample the signal +start (SG gen) = do + pool <- newIORef [] + S sample <- gen pool + return $ do + res <- sample + superstep pool + return res + +-- | Performing the two-phase superstep. +superstep :: IORef UpdatePool -> IO () +superstep pool = loop id [] + where + deref ptr = (fmap.fmap) ((,) ptr) (deRefWeak ptr) + loop getPtrs final = do + (ptrs,acts) <- unzip.catMaybes <$> (mapM deref =<< readIORef pool) + case acts of + [] -> do + sequence_ final + writeIORef pool (getPtrs []) + _ -> do + writeIORef pool [] + mapM_ fst acts + loop ((ptrs++) . getPtrs) (mapM_ snd acts : final) + +-- | Auxiliary function used by all the primitives that create a +-- mutable variable. +addSignal :: (a -> IO a) -- ^ sampling function + -> (a -> IO ()) -- ^ aging function + -> IORef (Phase a) -- ^ the mutable variable behind the signal + -> IORef UpdatePool -- ^ the pool of update actions + -> IO (Signal a) -- ^ the signal created +addSignal sample update ref pool = do + let upd = readIORef ref >>= \v -> case v of + Ready x -> update x + _ -> return () + + fin = readIORef ref >>= \v -> case v of + Updated x _ -> writeIORef ref $! Ready x + _ -> error "Signal not updated!" + + sig = S $ readIORef ref >>= \v -> case v of + Ready x -> sample x + Updated _ x -> return x + {-# NOINLINE sig #-} + -- NOINLINE to prevent sig from getting inlined into the + -- argument position of mkWeak. + + updateActions <- mkWeak sig (upd,fin) Nothing + modifyIORef pool (updateActions:) + return sig + +-- | The 'delay' combinator is the elementary building block for +-- adding state to the signal network by constructing delayed versions +-- of a signal that emit a given value at creation time and the +-- previous output of the signal afterwards (@--@ is undefined): +-- +-- > delay x0 s = <| <<x0 s0 s1 s2 s3 ...>> +-- > <<-- x0 s1 s2 s3 ...>> +-- > <<-- -- x0 s2 s3 ...>> +-- > <<-- -- -- x0 s3 ...>> +-- > ... +-- > |> +-- +-- It can be thought of as the following function (which should also +-- make it clear why the return value is 'SignalGen'): +-- +-- > delay x0 s t_start t_sample +-- > | t_start == t_sample = x0 +-- > | t_start < t_sample = s (t_sample-1) +-- > | otherwise = error \"Premature sample!\" +-- +-- The way signal generators are extracted by 'generator' ensures that +-- the error can never happen. +-- +-- Example (requires the @DoRec@ extension): +-- +-- > do +-- > smp <- start $ do +-- > rec let fib'' = liftA2 (+) fib' fib +-- > fib' <- delay 1 fib'' +-- > fib <- delay 1 fib' +-- > return fib +-- > res <- replicateM 7 smp +-- > print res +-- +-- Output: +-- +-- > [1,1,2,3,5,8,13] +delay :: a -- ^ initial output at creation time + -> Signal a -- ^ the signal to delay + -> SignalGen (Signal a) -- ^ the delayed signal +delay x0 (S s) = SG $ \pool -> do + ref <- newIORef (Ready x0) + + let update x = s >>= \x' -> x' `seq` writeIORef ref (Updated x' x) + + addSignal return update ref pool + +-- | A formal conversion from signals to signal generators, which +-- effectively allows for retrieving the current value of a previously +-- created signal within a generator. This includes both signals +-- defined in an external scope as well as those created earlier in +-- the same generator. In the model, it corresponds to the identity +-- function. +snapshot :: Signal a -> SignalGen a +snapshot (S s) = SG $ \_ -> s + +-- | Auxiliary function. +memoise :: IORef (Phase a) -> a -> IO a +memoise ref x = writeIORef ref (Updated undefined x) >> return x + +-- | A reactive signal that takes the value to output from a signal +-- generator carried by its input with the sampling time provided as +-- the start time for the generated structure. It is possible to +-- create new signals in the monad, which is the key to defining +-- dynamic data-flow networks. +-- +-- > generator << <|x00 x01 x02 ...|> +-- > <|x10 x11 x12 ...|> +-- > <|x20 x21 x22 ...|> +-- > ... +-- > >> = <| <<x00 x11 x22 ...>> +-- > <<x00 x11 x22 ...>> +-- > <<x00 x11 x22 ...>> +-- > ... +-- > |> +-- +-- It can be thought of as the following function: +-- +-- > generator g t_start t_sample = g t_sample t_sample +-- +-- It has to live in the 'SignalGen' monad, because it needs to +-- maintain an internal state to be able to cache the current sample +-- for efficiency reasons. However, this state is not carried between +-- samples, therefore start time doesn't matter and can be ignored. +-- +-- Refer to the longer example at the bottom to see how it can be +-- used. +generator :: Signal (SignalGen a) -- ^ the signal of generators to run + -> SignalGen (Signal a) -- ^ the signal of generated structures +generator (S s) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = (s >>= \(SG g) -> g pool) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Memoising combinator. It can be used to cache results of +-- applicative combinators in case they are used in several places. +-- It is observationally equivalent to 'return' in the 'SignalGen' +-- monad. +-- +-- > memo s = <|s s s s ...|> +-- +-- For instance, if @s = f \<$\> s'@, then @f@ will be recalculated +-- once for each sampling of @s@. This can be avoided by writing @s +-- \<- memo (f \<$\> s')@ instead. However, 'memo' incurs a small +-- overhead, therefore it should not be used blindly. +-- +-- All the functions defined in this module return memoised signals. +memo :: Signal a -- ^ the signal to cache + -> SignalGen (Signal a) -- ^ a signal observationally equivalent to the argument +memo (S s) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = s >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | A signal that is true exactly once: the first time the input +-- signal is true. Afterwards, it is constantly false, and it holds +-- no reference to the input signal. For instance (assuming the rest +-- of the input is constantly @False@): +-- +-- > till <<False False True True False True ...>> = +-- > <| <<False False True False False False False False False False ...>> +-- > << --- False True False False False False False False False ...>> +-- > << --- --- True False False False False False False False ...>> +-- > << --- --- --- True False False False False False False ...>> +-- > << --- --- --- --- False True False False False False ...>> +-- > << --- --- --- --- --- True False False False False ...>> +-- > << --- --- --- --- --- --- False False False False ...>> +-- > ... +-- > |> +-- +-- It is observationally equivalent to the following expression (which +-- would hold onto @s@ forever): +-- +-- > till s = do +-- > step <- transfer False (||) s +-- > dstep <- delay False step +-- > memo (liftA2 (/=) step dstep) +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > cnt <- stateful 0 (+1) +-- > tick <- till ((>=3) <$> cnt) +-- > return $ liftA2 (,) cnt tick +-- > res <- replicateM 6 smp +-- > print res +-- +-- Output: +-- +-- > [(0,False),(1,False),(2,False),(3,True),(4,False),(5,False)] +till :: Signal Bool -- ^ the boolean input signal + -> SignalGen (Signal Bool) -- ^ a one-shot signal true only the first time the input is true +till (S s) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + rsmp <- mfix $ \rs -> newIORef $ do + x <- s + writeIORef ref (Updated undefined x) + when x $ writeIORef rs $ do + writeIORef ref (Updated undefined False) + return False + return x + + let sample = join (readIORef rsmp) + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | A signal that can be directly fed through the sink function +-- returned. This can be used to attach the network to the outer +-- world. The signal always yields the value last written to the +-- sink. In other words, if the sink is written less frequently than +-- the network sampled, the output remains the same during several +-- samples. If values are pushed in the sink more frequently, only +-- the last one before sampling is visible on the output. +-- +-- Example: +-- +-- > do +-- > (sig,snk) <- unsafeExternal 4 +-- > smp <- start (return sig) +-- > r1 <- smp +-- > r2 <- smp +-- > snk 7 +-- > r3 <- smp +-- > snk 9 +-- > snk 2 +-- > r4 <- smp +-- > print [r1,r2,r3,r4] +-- +-- Output: +-- +-- > [4,4,7,2] +-- +-- There are two reasons why this construct is deemed unsafe. +-- Firstly, if the sink is used from another thread several times +-- during the sampling process, the observed value of the signal might +-- be inconsistent within a superstep. More interestingly, this +-- unmanaged channel can interact with 'snapshot' in strange ways. +-- See <https://github.com/cobbpg/elerea/issues/9> for some examples. +-- +-- Note: this function used to be called @external@ up until version 2.8.0. +unsafeExternal :: a -- ^ initial value + -> IO (Signal a, a -> IO ()) -- ^ the signal and an IO function to feed it +unsafeExternal x = do + ref <- newIORef x + return (S (readIORef ref), writeIORef ref) + +-- | A signal that can be directly fed through the sink function +-- returned. This can be used to attach the network to the outer +-- world. The signal always yields the value last written to the +-- sink at the start of the superstep. In other words, if the sink +-- is written less frequently than the network sampled, the output +-- remains the same during several samples. If values are pushed +-- in the sink more frequently, only the last one before sampling +-- is visible on the output. +-- +-- Example: +-- +-- > do +-- > (gen,snk) <- external 4 +-- > smp <- start gen +-- > r1 <- smp +-- > r2 <- smp +-- > snk 7 +-- > r3 <- smp +-- > snk 9 +-- > snk 2 +-- > r4 <- smp +-- > print [r1,r2,r3,r4] +-- +-- Output: +-- +-- > [4,4,7,2] +external :: a -- ^ initial value + -> IO (SignalGen (Signal a), a -> IO ()) -- ^ the generator to create the signal and an IO function to feed it +external x = do + ref <- newIORef x + return (SG $ \pool -> do + memoRef <- newIORef (Updated undefined x) + let sample = readIORef ref >>= memoise memoRef + addSignal (const sample) (const (() <$ sample)) memoRef pool + ,writeIORef ref + ) + +-- | An event-like signal that can be fed through the sink function +-- returned. The signal carries a list of values fed in since the +-- last sampling, i.e. it is constantly @[]@ if the sink is never +-- invoked. The order of elements is reversed, so the last value +-- passed to the sink is the head of the list. +-- +-- Example: +-- +-- > do +-- > (gen,snk) <- externalMulti +-- > smp <- start gen +-- > r1 <- smp +-- > snk 7 +-- > r2 <- smp +-- > r3 <- smp +-- > snk 9 +-- > snk 2 +-- > r4 <- smp +-- > print [r1,r2,r3,r4] +-- +-- Output: +-- +-- > [[],[7],[],[2,9]] +externalMulti :: IO (SignalGen (Signal [a]), a -> IO ()) -- ^ a generator for the event signal and the associated sink +externalMulti = do + var <- newMVar [] + return (SG $ \pool -> do + ref <- newIORef (Ready undefined) + let sample = modifyMVar var $ \list -> memoise ref list >> return ([], list) + addSignal (const sample) (const (() <$ sample)) ref pool + ,\val -> do vals <- takeMVar var + putMVar var (val:vals) + ) + +-- | A pure stateful signal. The initial state is the first output, +-- and every subsequent state is derived from the preceding one by +-- applying a pure transformation. +-- +-- Example: +-- +-- > do +-- > smp <- start (stateful "x" ('x':)) +-- > res <- replicateM 5 smp +-- > print res +-- +-- Output: +-- +-- > ["x","xx","xxx","xxxx","xxxxx"] +stateful :: a -- ^ initial state + -> (a -> a) -- ^ state transformation + -> SignalGen (Signal a) +stateful x0 f = mfix $ \sig -> delay x0 (f <$> sig) + +-- | A stateful transfer function. The current input affects the +-- current output, i.e. the initial state given in the first argument +-- is considered to appear before the first output, and can never be +-- observed, and subsequent states are determined by combining the +-- preceding state with the current output of the input signal using +-- the function supplied. +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > cnt <- stateful 1 (+1) +-- > transfer 10 (+) cnt +-- > res <- replicateM 5 smp +-- > print res +-- +-- Output: +-- +-- > [11,13,16,20,25] +transfer :: a -- ^ initial internal state + -> (t -> a -> a) -- ^ state updater function + -> Signal t -- ^ input signal + -> SignalGen (Signal a) +transfer x0 f s = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftA2 f s sig') + +-- | A variation of 'transfer' with two input signals. +transfer2 :: a -- ^ initial internal state + -> (t1 -> t2 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> SignalGen (Signal a) +transfer2 x0 f s1 s2 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftA3 f s1 s2 sig') + +-- | A variation of 'transfer' with three input signals. +transfer3 :: a -- ^ initial internal state + -> (t1 -> t2 -> t3 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> Signal t3 -- ^ input signal 3 + -> SignalGen (Signal a) +transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftM4 f s1 s2 s3 sig') + +-- | A variation of 'transfer' with four input signals. +transfer4 :: a -- ^ initial internal state + -> (t1 -> t2 -> t3 -> t4 -> a -> a) -- ^ state updater function + -> Signal t1 -- ^ input signal 1 + -> Signal t2 -- ^ input signal 2 + -> Signal t3 -- ^ input signal 3 + -> Signal t4 -- ^ input signal 4 + -> SignalGen (Signal a) +transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftM5 f s1 s2 s3 s4 sig') + +{- $effectful + +The following combinators are primarily aimed at library implementors +who wish build abstractions to effectful libraries on top of Elerea. + +-} + +-- | An IO action executed in the 'SignalGen' monad. Can be used as +-- `liftIO`. +execute :: IO a -> SignalGen a +execute act = SG $ \_ -> act + +-- | A signal that executes a given IO action once at every sampling. +-- +-- In essence, this combinator provides cooperative multitasking +-- capabilities, and its primary purpose is to assist library writers +-- in wrapping effectful APIs as conceptually pure signals. If there +-- are several effectful signals in the system, their order of +-- execution is undefined and should not be relied on. +-- +-- Example: +-- +-- > do +-- > smp <- start $ do +-- > ref <- execute $ newIORef 0 +-- > effectful $ do +-- > x <- readIORef ref +-- > putStrLn $ "Count: " ++ show x +-- > writeIORef ref $! x+1 +-- > return () +-- > replicateM_ 5 smp +-- +-- Output: +-- +-- > Count: 0 +-- > Count: 1 +-- > Count: 2 +-- > Count: 3 +-- > Count: 4 +-- +-- Another example (requires mersenne-random): +-- +-- > do +-- > smp <- start $ effectful randomIO :: IO (IO Double) +-- > res <- replicateM 5 smp +-- > print res +-- +-- Output: +-- +-- > [0.12067753390401374,0.8658877349182655,0.7159264443196786,0.1756941896012891,0.9513646060896676] +effectful :: IO a -- ^ the action to be executed repeatedly + -> SignalGen (Signal a) +effectful act = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = act >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | A signal that executes a parametric IO action once at every +-- sampling. The parameter is supplied by another signal at every +-- sampling step. +effectful1 :: (t -> IO a) -- ^ the action to be executed repeatedly + -> Signal t -- ^ parameter signal + -> SignalGen (Signal a) +effectful1 act (S s) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = s >>= act >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with two parameter signals. +effectful2 :: (t1 -> t2 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> SignalGen (Signal a) +effectful2 act (S s1) (S s2) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM2 act s1 s2) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with three parameter signals. +effectful3 :: (t1 -> t2 -> t3 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> Signal t3 -- ^ parameter signal 3 + -> SignalGen (Signal a) +effectful3 act (S s1) (S s2) (S s3) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM3 act s1 s2 s3) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | Like 'effectful1', but with four parameter signals. +effectful4 :: (t1 -> t2 -> t3 -> t4 -> IO a) -- ^ the action to be executed repeatedly + -> Signal t1 -- ^ parameter signal 1 + -> Signal t2 -- ^ parameter signal 2 + -> Signal t3 -- ^ parameter signal 3 + -> Signal t4 -- ^ parameter signal 4 + -> SignalGen (Signal a) +effectful4 act (S s1) (S s2) (S s3) (S s4) = SG $ \pool -> do + ref <- newIORef (Ready undefined) + + let sample = join (liftM4 act s1 s2 s3 s4) >>= memoise ref + + addSignal (const sample) (const (() <$ sample)) ref pool + +-- | The Show instance is only defined for the sake of Num... +instance Show (Signal a) where + showsPrec _ _ s = "<SIGNAL>" ++ s + +-- | Equality test is impossible. +instance Eq (Signal a) where + _ == _ = False + +-- | Error message for unimplemented instance functions. +unimp :: String -> a +unimp = error . ("Signal: "++) + +instance Ord t => Ord (Signal t) where + compare = unimp "compare" + min = liftA2 min + max = liftA2 max + +instance Enum t => Enum (Signal t) where + succ = fmap succ + pred = fmap pred + toEnum = pure . toEnum + fromEnum = unimp "fromEnum" + enumFrom = unimp "enumFrom" + enumFromThen = unimp "enumFromThen" + enumFromTo = unimp "enumFromTo" + enumFromThenTo = unimp "enumFromThenTo" + +instance Bounded t => Bounded (Signal t) where + minBound = pure minBound + maxBound = pure maxBound + +instance Num t => Num (Signal t) where + (+) = liftA2 (+) + (-) = liftA2 (-) + (*) = liftA2 (*) + signum = fmap signum + abs = fmap abs + negate = fmap negate + fromInteger = pure . fromInteger + +instance Real t => Real (Signal t) where + toRational = unimp "toRational" + +instance Integral t => Integral (Signal t) where + quot = liftA2 quot + rem = liftA2 rem + div = liftA2 div + mod = liftA2 mod + quotRem a b = (fst <$> qrab,snd <$> qrab) + where qrab = quotRem <$> a <*> b + divMod a b = (fst <$> dmab,snd <$> dmab) + where dmab = divMod <$> a <*> b + toInteger = unimp "toInteger" + +instance Fractional t => Fractional (Signal t) where + (/) = liftA2 (/) + recip = fmap recip + fromRational = pure . fromRational + +instance Floating t => Floating (Signal t) where + pi = pure pi + exp = fmap exp + sqrt = fmap sqrt + log = fmap log + (**) = liftA2 (**) + logBase = liftA2 logBase + sin = fmap sin + tan = fmap tan + cos = fmap cos + asin = fmap asin + atan = fmap atan + acos = fmap acos + sinh = fmap sinh + tanh = fmap tanh + cosh = fmap cosh + asinh = fmap asinh + atanh = fmap atanh + acosh = fmap acosh + +{- $example + +For a not entirely trivial example, let's create a dynamic collection +of countdown timers, where each expired timer is removed from the +collection. First of all, we'll need a simple tester function: + +@ + sigtest gen = 'replicateM' 15 '=<<' 'start' gen +@ + +We can try it with a trivial example: + +@ + \> sigtest $ 'stateful' 2 (+3) + [2,5,8,11,14,17,20,23,26,29,32,35,38,41,44,47] +@ + +Our first definition will be a signal representing a simple named +timer: + +@ + countdown :: String -\> Int -\> SignalGen (Signal (String,Maybe Int)) + countdown name t = do + let tick prev = do { t \<- prev ; 'guard' (t \> 0) ; 'return' (t-1) } + timer \<- 'stateful' (Just t) tick + 'return' ((,) name '<$>' timer) +@ + +Let's see if it works: + +@ + \> sigtest $ countdown \"foo\" 4 + [(\"foo\",Just 4),(\"foo\",Just 3),(\"foo\",Just 2),(\"foo\",Just 1),(\"foo\",Just 0), + (\"foo\",Nothing),(\"foo\",Nothing),(\"foo\",Nothing),...] +@ + +Next, we will define a timer source that takes a list of timer names, +starting values and start times and creates a signal that delivers the +list of new timers at every point: + +@ + timerSource :: [(String, Int, Int)] -\> SignalGen (Signal [Signal (String, Maybe Int)]) + timerSource ts = do + let gen t = 'mapM' ('uncurry' countdown) newTimers + where newTimers = [(n,v) | (n,v,st) \<- ts, st == t] + cnt \<- 'stateful' 0 (+1) + 'generator' (gen '<$>' cnt) +@ + +Now we need to encapsulate the timer source signal in another signal +expression that takes care of maintaining the list of live timers. +Since working with dynamic collections is a recurring task, let's +define a generic combinator that maintains a dynamic list of signals +given a source and a test that tells from the output of each signal +whether it should be kept. We can use @mdo@ expressions (a variant of +@do@ expressions allowing forward references) as syntactic sugar for +'mfix' to make life easier: + +@ + collection :: Signal [Signal a] -\> (a -\> Bool) -\> SignalGen (Signal [a]) + collection source isAlive = mdo + sig \<- 'delay' [] ('map' 'snd' '<$>' collWithVals') + coll \<- 'memo' ('liftA2' (++) source sig) + let collWithVals = 'zip' '<$>' ('sequence' '=<<' coll) '<*>' coll + collWithVals' \<- 'memo' ('filter' (isAlive . 'fst') '<$>' collWithVals) + 'return' $ 'map' 'fst' '<$>' collWithVals' +@ + +We need recursion to define the @coll@ signal as a delayed version of +its continuation, which does not contain signals that need to be +removed in the current sample. At every point of time the running +collection is concatenated with the source. We define @collWithVals@, +which simply pairs up every signal with its current output. The +output is obtained by extracting the current value of the signal +container and sampling each element with 'sequence'. We can then +derive @collWithVals'@, which contains only the signals that must be +kept for the next round along with their output. Both @coll@ and +@collWithVals'@ have to be memoised, because they are used more than +once (the program would work without that, but it would recalculate +both signals each time they are used). By throwing out the respective +parts, we can get both the final output and the collection for the +next step (@coll'@). + +Now we can easily finish the original task: + +@ + timers :: [(String, Int, Int)] -\> SignalGen (Signal [(String, Int)]) + timers timerData = do + src \<- timerSource timerData + getOutput '<$>' collection src ('isJust' . 'snd') + where getOutput = 'fmap' ('map' (\\(name,Just val) -> (name,val))) +@ + +As a test, we can start four timers: /a/ at t=0 with value 3, /b/ and +/c/ at t=1 with values 5 and 3, and /d/ at t=3 with value 4: + +@ + \> sigtest $ timers [(\"a\",3,0),(\"b\",5,1),(\"c\",3,1),(\"d\",4,3)] + [[(\"a\",3)],[(\"b\",5),(\"c\",3),(\"a\",2)],[(\"b\",4),(\"c\",2),(\"a\",1)], + [(\"d\",4),(\"b\",3),(\"c\",1),(\"a\",0)],[(\"d\",3),(\"b\",2),(\"c\",0)], + [(\"d\",2),(\"b\",1)],[(\"d\",1),(\"b\",0)],[(\"d\",0)],[],[],[],[],[],[],[]] +@ + +If the noise of the applicative lifting operators feels annoying, she +(<http://personal.cis.strath.ac.uk/~conor/pub/she/>) comes to the +save. Among other features it provides idiom brackets, which can +substitute the explicit lifting. For instance, it allows us to define +@collection@ this way: + +@ + collection :: Stream [Stream a] -> (a -> Bool) -> StreamGen (Stream [a]) + collection source isAlive = mdo + sig \<- 'delay' [] (|'map' ~'snd' collWithVals'|) + coll \<- 'memo' (|source ++ sig|) + collWithVals' \<- 'memo' (|'filter' ~(isAlive . 'fst') (|'zip' ('sequence' '=<<' coll) coll|)|) + 'return' (|'map' ~'fst' collWithVals'|) +@ + +-}
FRP/Elerea/Simple/Pure.hs view
@@ -1,85 +1,85 @@-{-|--This module contains the reference implementation for the pure subset-of the simple variant of Elerea. I/O embedding is substituted by-conversion from and to lists.---}--module FRP.Elerea.Simple.Pure- (- -- * The signal abstraction- Signal- , SignalGen- -- * Inputs and outputs- , fromList- , toList- , start- -- * Basic building blocks- , delay- , snapshot- , generator- , memo- , until- -- * Derived combinators- , stateful- , transfer- , transfer2- , transfer3- , transfer4- ) where--import Control.Applicative-import Control.Monad-import Control.Monad.Fix--type Signal a = Int -> a--type SignalGen a = Int -> a--fromList :: [a] -> Signal a-fromList = (!!)--toList :: Signal a -> [a]-toList s = map s [0..]--start :: SignalGen (Signal a) -> [a]-start g = toList (g 0)--delay :: a -> Signal a -> SignalGen (Signal a)-delay x0 s t_start t_sample- | t_start < t_sample = s (t_sample-1)- | t_start == t_sample = x0- | otherwise = error "This signal doesn't exist yet."--generator :: Signal (SignalGen a) -> SignalGen (Signal a)-generator s _t_start t_sample = s t_sample t_sample--snapshot :: Signal a -> SignalGen a-snapshot = id--memo :: Signal a -> SignalGen (Signal a)-memo = const--stateful :: a -> (a -> a) -> SignalGen (Signal a)-stateful x0 f = mfix $ \sig -> delay x0 (f <$> sig)--transfer :: a -> (t -> a -> a) -> Signal t -> SignalGen (Signal a)-transfer x0 f s = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA2 f s sig')--transfer2 :: a -> (t1 -> t2 -> a -> a) -> Signal t1 -> Signal t2 -> SignalGen (Signal a)-transfer2 x0 f s1 s2 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftA3 f s1 s2 sig')--transfer3 :: a -> (t1 -> t2 -> t3 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> SignalGen (Signal a)-transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM4 f s1 s2 s3 sig')--transfer4 :: a -> (t1 -> t2 -> t3 -> t4 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> Signal t4 -> SignalGen (Signal a)-transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do- sig' <- delay x0 sig- memo (liftM5 f s1 s2 s3 s4 sig')+{-| + +This module contains the reference implementation for the pure subset +of the simple variant of Elerea. I/O embedding is substituted by +conversion from and to lists. + +-} + +module FRP.Elerea.Simple.Pure + ( + -- * The signal abstraction + Signal + , SignalGen + -- * Inputs and outputs + , fromList + , toList + , start + -- * Basic building blocks + , delay + , snapshot + , generator + , memo + , until + -- * Derived combinators + , stateful + , transfer + , transfer2 + , transfer3 + , transfer4 + ) where + +import Control.Applicative +import Control.Monad +import Control.Monad.Fix + +type Signal a = Int -> a + +type SignalGen a = Int -> a + +fromList :: [a] -> Signal a +fromList = (!!) + +toList :: Signal a -> [a] +toList s = map s [0..] + +start :: SignalGen (Signal a) -> [a] +start g = toList (g 0) + +delay :: a -> Signal a -> SignalGen (Signal a) +delay x0 s t_start t_sample + | t_start < t_sample = s (t_sample-1) + | t_start == t_sample = x0 + | otherwise = error "This signal doesn't exist yet." + +generator :: Signal (SignalGen a) -> SignalGen (Signal a) +generator s _t_start t_sample = s t_sample t_sample + +snapshot :: Signal a -> SignalGen a +snapshot = id + +memo :: Signal a -> SignalGen (Signal a) +memo = const + +stateful :: a -> (a -> a) -> SignalGen (Signal a) +stateful x0 f = mfix $ \sig -> delay x0 (f <$> sig) + +transfer :: a -> (t -> a -> a) -> Signal t -> SignalGen (Signal a) +transfer x0 f s = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftA2 f s sig') + +transfer2 :: a -> (t1 -> t2 -> a -> a) -> Signal t1 -> Signal t2 -> SignalGen (Signal a) +transfer2 x0 f s1 s2 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftA3 f s1 s2 sig') + +transfer3 :: a -> (t1 -> t2 -> t3 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> SignalGen (Signal a) +transfer3 x0 f s1 s2 s3 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftM4 f s1 s2 s3 sig') + +transfer4 :: a -> (t1 -> t2 -> t3 -> t4 -> a -> a) -> Signal t1 -> Signal t2 -> Signal t3 -> Signal t4 -> SignalGen (Signal a) +transfer4 x0 f s1 s2 s3 s4 = mfix $ \sig -> do + sig' <- delay x0 sig + memo (liftM5 f s1 s2 s3 s4 sig')
LICENSE view
@@ -1,28 +1,28 @@-Copyright (c) 2009-2010, Patai Gergely-All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:--1. Redistributions of source code must retain the above copyright notice,- this list of conditions and the following disclaimer.--2. 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.--3. Neither the name of the author nor the names of its contributors may be- used to endorse or promote products derived from this software without- specific prior written permission.--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 OWNER 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.+Copyright (c) 2009-2010, Patai Gergely +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the author nor the names of its contributors may be + used to endorse or promote products derived from this software without + specific prior written permission. + +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 OWNER 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.
Setup.hs view
@@ -1,3 +1,3 @@-import Distribution.Simple--main = defaultMain+import Distribution.Simple + +main = defaultMain
elerea.cabal view
@@ -1,61 +1,58 @@-Name: elerea-Version: 2.8.0-Cabal-Version: >= 1.2-Synopsis: A minimalistic FRP library-Category: reactivity, FRP-Description:-- Elerea (Eventless reactivity) is a tiny discrete time FRP- implementation without the notion of event-based switching and- sampling, with first-class signals (time-varying values). Reactivity- is provided through various higher-order constructs that also allow- the user to work with arbitrary time-varying structures containing- live signals. Signals have precise and simple denotational- semantics.- .- Stateful signals can be safely generated at any time through a- monadic interface, while stateless combinators can be used in a- purely applicative style. Elerea signals can be defined recursively,- and external input is trivial to attach. The library comes in three- major variants:- .- * Simple: signals are plain discrete streams isomorphic to functions- over natural numbers;- .- * Param: adds a globally accessible input signal for convenience;- .- * Clocked: adds the ability to freeze whole subnetworks at will.- .- This is a minimal library that defines only some basic primitives,- and you are advised to install @elerea-examples@ as well to get an- idea how to build non-trivial systems with it. The examples are- separated in order to minimise the dependencies of the core library.- The @dow@ package contains a full game built on top of the simple- variant.- .- The basic idea of the implementation is described in the WFLP 2010- paper /Efficient and Compositional Higher-Order Streams/- (<http://sgate.emt.bme.hu/documents/patai/publications/PataiWFLP2010.pdf>).- .- Additional contributions: Takano Akio, Mitsutoshi Aoe--Author: Patai Gergely-Maintainer: Patai Gergely (patai.gergely@gmail.com)-Copyright: (c) 2009-2012, Patai Gergely-License: BSD3-License-File: LICENSE-Stability: experimental-Build-Type: Simple-Extra-Source-Files:- CHANGES--Library- Exposed-Modules:- FRP.Elerea- FRP.Elerea.Simple- FRP.Elerea.Simple.Pure- FRP.Elerea.Param- FRP.Elerea.Clocked-- Build-Depends: base >= 4 && < 5, containers, transformers, transformers-base- ghc-options: -Wall -O2+Name: elerea +Version: 2.9.0 +Cabal-Version: >= 1.2 +Synopsis: A minimalistic FRP library +Category: reactivity, FRP +Description: + + Elerea (Eventless reactivity) is a tiny discrete time FRP + implementation without the notion of event-based switching and + sampling, with first-class signals (time-varying values). Reactivity + is provided through various higher-order constructs that also allow + the user to work with arbitrary time-varying structures containing + live signals. Signals have precise and simple denotational + semantics. + . + Stateful signals can be safely generated at any time through a + monadic interface, while stateless combinators can be used in a + purely applicative style. Elerea signals can be defined recursively, + and external input is trivial to attach. The library comes in two + major variants: + . + * Simple: signals are plain discrete streams isomorphic to functions + over natural numbers; + . + * Param: adds a globally accessible input signal for convenience; + . + This is a minimal library that defines only some basic primitives, + and you are advised to install @elerea-examples@ as well to get an + idea how to build non-trivial systems with it. The examples are + separated in order to minimise the dependencies of the core library. + The @dow@ package contains a full game built on top of the simple + variant. + . + The basic idea of the implementation is described in the WFLP 2010 + paper /Efficient and Compositional Higher-Order Streams/ + (<http://sgate.emt.bme.hu/documents/patai/publications/PataiWFLP2010.pdf>). + . + Additional contributions: Takano Akio, Mitsutoshi Aoe + +Author: Patai Gergely +Maintainer: Patai Gergely (patai.gergely@gmail.com) +Copyright: (c) 2009-2012, Patai Gergely +License: BSD3 +License-File: LICENSE +Stability: experimental +Build-Type: Simple +Extra-Source-Files: + CHANGES + +Library + Exposed-Modules: + FRP.Elerea + FRP.Elerea.Simple + FRP.Elerea.Simple.Pure + FRP.Elerea.Param + + Build-Depends: base >= 4 && < 5, containers, transformers, transformers-base + ghc-options: -Wall -O2