{-# LANGUAGE TypeFamilies, RecursiveDo, FlexibleContexts, ScopedTypeVariables #-} -- | Generalization of the Sodium API to allow for parallel processing. module FRP.Sodium.Context where import Control.Applicative import Control.Monad import Control.Monad.Fix import Data.Monoid class ( Applicative (Reactive r), Monad (Reactive r), MonadFix (Reactive r), Functor (Event r), Applicative (Behavior r) ) => Context r where -- | A monad for transactional reactive operations. Execute it from 'IO' using 'sync'. data Reactive r :: * -> * -- | A stream of events. The individual firings of events are called \'event occurrences\'. data Event r :: * -> * -- | A time-varying value, American spelling. data Behavior r :: * -> * -- | Execute the specified 'Reactive' within a new transaction, blocking the caller -- until all resulting processing is complete and all callbacks have been called. -- This operation is thread-safe, so it may be called from any thread. -- -- State changes to 'hold' values occur after processing of the transaction is complete. sync :: Reactive r a -> IO a -- | Returns an event, and a push action for pushing a value into the event. newEvent :: Reactive r (Event r a, a -> Reactive r ()) -- | Listen for firings of this event. The returned @IO ()@ is an IO action -- that unregisters the listener. This is the observer pattern. -- -- To listen to a 'Behavior' use @listen (value b) handler@ listen :: Event r a -> (a -> IO ()) -> Reactive r (IO ()) -- | An event that never fires. never :: Event r a -- | Merge two streams of events of the same type. -- -- In the case where two event occurrences are simultaneous (i.e. both -- within the same transaction), both will be delivered in the same -- transaction. If the event firings are ordered for some reason, then -- their ordering is retained. In many common cases the ordering will -- be undefined. merge :: Event r a -> Event r a -> Event r a -- | Unwrap Just values, and discard event occurrences with Nothing values. filterJust :: Event r (Maybe a) -> Event r a -- | Create a behavior with the specified initial value, that gets updated -- by the values coming through the event. The \'current value\' of the behavior -- is notionally the value as it was 'at the start of the transaction'. -- That is, state updates caused by event firings get processed at the end of -- the transaction. hold :: a -> Event r a -> Reactive r (Behavior r a) -- | An event that gives the updates for the behavior. If the behavior was created -- with 'hold', then 'updates' gives you an event equivalent to the one that was held. updates :: Behavior r a -> Event r a -- | An event that is guaranteed to fire once when you listen to it, giving -- the current value of the behavior, and thereafter behaves like 'changes', -- firing for each update to the behavior's value. value :: Behavior r a -> Event r a -- | Sample the behavior at the time of the event firing. Note that the 'current value' -- of the behavior that's sampled is the value as at the start of the transaction -- before any state changes of the current transaction are applied through 'hold's. snapshot :: (a -> b -> c) -> Event r a -> Behavior r b -> Event r c -- | Unwrap an event inside a behavior to give a time-varying event implementation. switchE :: Behavior r (Event r a) -> Event r a -- | Unwrap a behavior inside another behavior to give a time-varying behavior implementation. switch :: Behavior r (Behavior r a) -> Reactive r (Behavior r a) -- | Execute the specified 'Reactive' action inside an event. execute :: Event r (Reactive r a) -> Event r a -- | Obtain the current value of a behavior. sample :: Behavior r a -> Reactive r a -- | If there's more than one firing in a single transaction, combine them into -- one using the specified combining function. -- -- If the event firings are ordered, then the first will appear at the left -- input of the combining function. In most common cases it's best not to -- make any assumptions about the ordering, and the combining function would -- ideally be commutative. coalesce :: (a -> a -> a) -> Event r a -> Event r a -- | Throw away all event occurrences except for the first one. once :: Event r a -> Event r a -- | Take each list item and put it into a new transaction of its own. -- -- An example use case of this might be a situation where we are splitting -- a block of input data into frames. We obviously want each frame to have -- its own transaction so that state is updated separately each frame. split :: Event r [a] -> Event r a class Context r => ContextIO r where -- | Execute the specified IO operation asynchronously on a separate thread, and -- signal the output event in a new transaction upon its completion. -- -- Caveat: Where 'switch' or 'switchE' is used, when some reactive logic has been -- switched away, we rely on garbage collection to actually disconnect this logic -- from any input it may be listening to. With normal Sodium code, everything is -- pure, so before garbage collection happens, the worst we will get is some wasted -- CPU cycles. If you are using 'executeAsyncIO'/'executeSyncIO' inside a 'switch' -- or 'switchE', however, it is possible that logic that has been switched away -- hasn't been garbage collected yet. This logic /could/ still run, and if it has -- observable effects, you could see it running after it is supposed to have been -- switched out. One way to avoid this is to pipe the source event for IO out of the -- switch, run the 'executeAsyncIO'/'executeSyncIO' outside the switch, and pipe its -- output back into the switch contents. executeAsyncIO :: Event r (IO a) -> Event r a -- | Execute the specified IO operation synchronously and fire the output event -- in the same transaction. -- -- Caveat: See 'executeAsyncIO'. executeSyncIO :: Event r (IO a) -> Event r a instance Context r => Monoid (Event r a) where mempty = never mappend = merge -- | A time-varying value, British spelling. type Behaviour r a = Behavior r a -- | Create a new 'Behavior' along with an action to push changes into it. -- American spelling. newBehavior :: forall r a . Context r => a -- ^ Initial behavior value -> Reactive r (Behavior r a, a -> Reactive r ()) newBehavior initA = do (ev, push) <- newEvent beh <- hold initA ev return (beh, push) -- | Create a new 'Behavior' along with an action to push changes into it. -- British spelling. newBehaviour :: forall r a . Context r => a -- ^ Initial behavior value -> Reactive r (Behavior r a, a -> Reactive r ()) newBehaviour = newBehavior -- | Merge two streams of events of the same type, combining simultaneous -- event occurrences. -- -- In the case where multiple event occurrences are simultaneous (i.e. all -- within the same transaction), they are combined using the same logic as -- 'coalesce'. mergeWith :: Context r => (a -> a -> a) -> Event r a -> Event r a -> Event r a mergeWith f ea eb = coalesce f $ merge ea eb -- | Only keep event occurrences for which the predicate returns true. filterE :: Context r => (a -> Bool) -> Event r a -> Event r a filterE pred = filterJust . ((\a -> if pred a then Just a else Nothing) <$>) -- | Let event occurrences through only when the behavior's value is True. -- Note that the behavior's value is as it was at the start of the transaction, -- that is, no state changes from the current transaction are taken into account. gate :: Context r => Event r a -> Behavior r Bool -> Event r a gate ea = filterJust . snapshot (\a b -> if b then Just a else Nothing) ea -- | Transform an event with a generalized state loop (a mealy machine). The function -- is passed the input and the old state and returns the new state and output value. collectE :: Context r => (a -> s -> (b, s)) -> s -> Event r a -> Reactive r (Event r b) collectE f z ea = do rec s <- hold z es let ebs = snapshot f ea s eb = fst <$> ebs es = snd <$> ebs return eb -- | Transform a behavior with a generalized state loop (a mealy machine). The function -- is passed the input and the old state and returns the new state and output value. collect :: Context r => (a -> s -> (b, s)) -> s -> Behavior r a -> Reactive r (Behavior r b) collect f zs bea = do let ea = coalesce (flip const) (updates bea) za <- sample bea let (zb, zs') = f za zs rec bs <- hold (zb, zs') ebs let ebs = snapshot f ea (snd <$> bs) return (fst <$> bs) -- | Accumulate state changes given in the input event. accum :: Context r => a -> Event r (a -> a) -> Reactive r (Behavior r a) accum z efa = do rec s <- hold z $ snapshot ($) efa s return s