\section{Edge Detection in FRP programs: RSAGL.Edge} Edge detection works for all inputs that implement Eq, and is simply a mechanism to observe when a value changes. \begin{code} {-# OPTIONS_GHC -farrows -fglasgow-exts #-} module RSAGL.Edge (Edge(..), edge, edgeBy, edgep, edgeFold, edgeFoldBy, edgeMap, edgeMapBy, history, sticky, initial, started) where import RSAGL.AbstractVector import RSAGL.FRP as FRP import RSAGL.SwitchedArrow as SwitchedArrow import RSAGL.Time import Control.Arrow import Control.Arrow.Operations import Control.Arrow.Transformer \end{code} \subsection{Edge data structure} \begin{code} data Edge a = Edge { edge_previous :: !a, edge_next :: !a, edge_changed :: !Time } deriving (Eq,Show) \end{code} \subsection{Edge detectors} \texttt{edge} watches an input and answers an Edge data structure. \begin{code} edge :: (Arrow a,ArrowChoice a,ArrowApply a,Eq e) => FRPX any t i o a e (Edge e) edge = edgeBy (==) edgeBy :: (Arrow a,ArrowChoice a,ArrowApply a) => (e -> e -> Bool) -> FRPX any t i o a e (Edge e) edgeBy predicate = proc e -> do t <- threadTime -< () edgeFoldBy (\x y -> predicate (fst x) (fst y)) (\(e,t) -> Edge e e t) edgeBy' -< (e,t) where edgeBy' (e,t) old_edge = Edge { edge_previous = edge_next old_edge, edge_next = e, edge_changed = t } \end{code} \texttt{edgep} answers True exactly once each time a value changes. \begin{code} edgep :: (Arrow a,ArrowChoice a,ArrowApply a,Eq e) => FRPX any t i o a e Bool edgep = FRP.statefulContext $ SwitchedArrow.withState edgep' id where edgep' = proc i -> do old_value <- lift fetch -< () lift store -< i returnA -< i /= old_value \end{code} \subsection{Edge folds} \texttt{edgeFold} combines each unique input into a cumulative value using a folding function. The folding function must have some way to discard old edges, or it will represent a space leak. \begin{code} edgeFold :: (Arrow a,ArrowChoice a,ArrowApply a,Eq j) => (j -> p) -> (j -> p -> p) -> FRPX any t i o a j p edgeFold = edgeFoldBy (==) edgeFoldBy :: (Arrow a,ArrowChoice a,ArrowApply a) => (j -> j -> Bool) -> (j -> p) -> (j -> p -> p) -> FRPX any t i o a j p edgeFoldBy predicate initialF f = FRP.statefulContext $ SwitchedArrow.withState edgeFold' (\i -> (i,initialF i)) where edgeFold' = proc i -> do (old_raw,old_folded) <- lift fetch -< () let (new_raw,new_folded) = if old_raw `predicate` i then (old_raw,old_folded) else (i,f i old_folded) lift store -< seq new_raw $ seq new_folded $ (new_raw,new_folded) returnA -< new_folded \end{code} \texttt{history} answers a history of edges for a value. The Time parameter indicates the maximum age of an edge, after which it can be forgotten. \begin{code} history :: (Arrow a,ArrowChoice a,ArrowApply a,Eq e) => Time -> FRPX any t i o a e [Edge e] history t = edgeFold (\x -> [x]) history' <<< edge where history' n h = n : takeWhile ((>= edge_changed n `sub` t) . edge_changed) h \end{code} \texttt{edgeMap} is equivalent to \texttt{arr}, but more efficient, possibly, because the mapping function is only applied when the input changes. \texttt{edgeMap} should actually be more efficient only when: the cost of the mapping function is high, the cost of comparing two equal values is low, and the input changes infrequently. \begin{code} edgeMap :: (Arrow a,ArrowChoice a,ArrowApply a,Eq j) => (j -> p) -> FRPX any t i o a j p edgeMap f = edgeMapBy (==) f edgeMapBy :: (Arrow a,ArrowChoice a,ArrowApply a,Eq j) => (j -> j -> Bool) -> (j -> p) -> FRPX any t i o a j p edgeMapBy predicate f = edgeFoldBy predicate f (const . f) \end{code} \texttt{sticky} remembers the most recent value of an input that satisfies some criteria. \begin{code} sticky :: (Arrow a,ArrowChoice a,ArrowApply a) => (x -> Bool) -> x -> FRPX any t i o a x x sticky criteria initial_value = edgeFoldBy (const $ const False) (const initial_value) (\new old -> if criteria new then new else old) \end{code} \subsection{Remembering the initial value of an input} \texttt{initial} answers the first value that an input ever has (during this instance of this thread). \begin{code} initial :: (Arrow a,ArrowChoice a,ArrowApply a,Eq e) => FRPX any t i o a e e initial = edgeMapBy (const $ const True) id \end{code} \texttt{started} is the \texttt{absoluteTime} at which this thread started. \begin{code} started :: (Arrow a,ArrowChoice a,ArrowApply a) => FRPX any t i o a () Time started = initial <<< absoluteTime \end{code}