{-# LANGUAGE TypeFamilies, DoRec, 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 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 -- Lift an arbitrary IO action into a 'Reactive'. ioReactive :: IO a -> Reactive r 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 (values 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. It doesn't do any equality -- comparison as the name might imply. changes :: 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. values :: 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. snapshotWith :: (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 :: Context r => Event r a -> Event r a -- | 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) <$>) -- | Variant of snapshotWith that throws away the event's value and captures the behavior's. snapshot :: Context r => Event r a -> Behavior r b -> Event r b snapshot = snapshotWith (flip const) -- | 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 . snapshotWith (\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 = snapshotWith 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) (changes bea) za <- sample bea let (zb, zs') = f za zs rec bs <- hold (zb, zs') ebs let ebs = snapshotWith 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 $ snapshotWith ($) efa s return s -- | Count event occurrences, giving a behavior that starts with 0 before the first occurrence. count :: Context r => Event r a -> Reactive r (Behavior r Int) count = accum 0 . (const (1+) <$>)