In a previous post, I presented a fundamental reason why classic FRP does not fit interactive behavior, which is that the semantic model captures only the influence of time and not other input. I also gave a simple alternative, with a simple and general model for temporal and spatial transformation, in which input behavior is transformed inversely to the transformation of output behavior.
The semantic model I suggested is the same as used in “Arrow FRP”, from Fruit and Yampa. I want, however, a more convenient and efficient way to package up that model, which is the subject of the post you are reading now.
Next, we took a close look at one awkward aspect of classic FRP for interactive behavior, namely the need to trim inputs, and how trimming relates to comonadic FRP.
trim function allows us to define multi-phase interactive behaviors correctly and efficiently, but its use is tedious and is easy to get wrong.
It thus fails to achieve what I want from functional programming in general and FRP in particular, which is to enable writing simple, natural descriptions, free of mechanical details.
The current post hides and automates the mechanics of trimming, so that the intent of an interactive behavior can be expressed directly and executed correctly and efficiently.
As before, I’ll adopt two abbreviations, for succinctness:
type B = Behavior type E = Event
Safe and easy trimming
Previously, I defined an interactive cat behavior as follows:
cat3 :: B World -> B Cat cat3 world = sleep world `switcher` ((uncurry prowl <$> trimf wake world) `mappend` (uncurry eat <$> trimf hunger world))
I’d really like to write the following
-- ideal: cat4 = sleep `switcher` ((prowl <$> wake) `mappend` (eat <$> hunger))
Let’s see how close we can get.
I can see right off I’ll have to replace or generalize
For now, I’ll replace it:
switcherf :: (B i -> B o) -> (B i -> E (B i -> B o)) -> (B i -> B o)
This function will have to manage trimming:
bf `switcherf` ef = i -> bf i `switcher` (uncurry ($) <$> trimf ef i)
I won’t have to replace
mappend, since it’s a method and so can have a variety of types.
In this case,
mappend applies to a function from behaviors to events.
Fortunately, the function monoid is exactly what we need:
instance Monoid b => Monoid (a -> b) where mempty = const mempty f `mappend` g = a -> f a `mappend` f b
or the more lovely standard form for applicative functors:
instance Monoid b => Monoid (a -> b) where mempty = pure mempty mappend = liftA2 mappend
The use of
cat4 above won’t work.
We want instead to
fmap inside the result of a function from behaviors to events.
Using the style of semantic editor combinators, we get the following definition, which is fairly close to our ideal:
cat4 = sleep `switcherf` ((result.fmap) prowl wake `mappend` (result.fmap) eat hunger)
switcher, introduce a new type class:
class Switchable b e where switcher :: b -> e b -> b
The original Reactive
switcher is a special case:
instance Switchable (B a) E where switcher = R.switcher
We can switch among tuples and among other containers of switchables. For instance,
instance (Functor e, Switchable b e, Switchable b' e) => Switchable (b,b') e where (b,b') `switcher` e = ( b `switcher` (fst <$> e) , b' `switcher` (snd <$> e) )
Looking through the examples above, all we really had to do with the input behavior was to compute all remainders. I used
duplicate :: B a -> B (B a)
class Temporal a where remainders :: a -> B a
Behaviors and events are included as a special case,
instance Temporal (B a) where remainders = duplicate instance Temporal (E a) where remainders = ...
Temporal values combine without losing their individuality, which allows efficient change-driven evaluation as in Simply efficient functional reactivity:
instance (Temporal a, Temporal b) => Temporal (a,b) where remainders (a,b) = remainders a `zip` remainders b
Similarly for other triples and other data structures.
Sometimes it’s handy to carry static information along with dynamic information. Static types can be made trivially temporal:
instance Temporal Bool where remainders = pure instance Temporal Int where remainders = pure -- etc
Temporal class, the trimming definitions above have more general types.
trim :: Temporal i => E o -> i -> E (o, i) trimf :: Temporal i => (i -> E o) -> (i -> E (o, i))
As does function switching:
switcherf :: (Temporal i, Switchable o E) => (i -> o) -> (i -> E (i -> o)) -> i -> o
Types for functional interactive behavior
We’ve gotten almost to my ideal cat definition.
We cannot, however, use
(<$>) here with functions from behaviors to behaviors, because the types don’t fit.
To cross the last gap, let’s define new types corresponding to the idioms we’ve seen repeatedly above.
-- First try type i :~> o = BI (B i -> B o) type i :-> o = EI (B i -> E o)
Or, using type composition:
-- Second try type (:~>) i = (->) (B i) :. B type (:->) i = (->) (B i) :. E
The advantage of type composition is that we get some useful definitions for free, including
However, there’s a problem with both versions. They limit us to a single behavior as input. A realistic interactive environment has many inputs, including a mixture of behaviors and events.
In Yampa, that mixture is combined into a single behavior, leading to two difficulties:
- The distinction between behaviors and events gets lost, as well as (I think) accurate and minimal-latency event detection and response.
- The bundled input environment changes whenever any component changes, leading to everything getting recomputed and redisplayed when anything changes.
To avoid these problems, I’ll take a different approach.
Generalize inputs from behaviors to arbitrary
Temporal values, which include behaviors, events and tuples and structures of temporal values.
The types for interactive behaviors and interactive events are
type (:~>) i = (->) i :. B type (:->) i = (->) i :. E
i :~> o is like
i -> B o, and
i :-> o is like
i -> E o.
Switching for interactive behaviors wraps the
switcherf function from above:
instance Temporal i => Switchable (i :~> o) ((:->) i) where switcher = inO2 $ bf ef -> bf `switcherf` (result.fmap) unO ef
This definition is actually more general than the type given here.
For instance, it can be used to switch between interactive events as well as interactive behaviors.
To see the generalization, first abstract out the commonality between
type i :->. f = (->) i :. f type (:~>) i = i :->. B type (:->) i = i :->. E
The same instance code but with a more general type:
instance (Temporal i, Switchable (f o) E) => Switchable ((i :->. f) o) ((:->) i) where switcher = inO2 $ bf ef -> bf `switcherf` (result.fmap) unO ef
We can also switch between interactive collections of behaviors and events, though not with the
Where are we?
Almost all of the pieces are in place now. Another post will relate input trimming to the time transformation of interactive behaviors, as discussed in Why classic FRP does not fit interactive behavior. Also, how interactive FRP relates to Sequences, segments, and signals.