sodium-0.6: src/FRP/Sodium/Context.hs
{-# 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+) <$>)