Functional interactive behavior
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.
The 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 switcher
.
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 (<$>)
(i.e., fmap
) in 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)
Generalized switching
To generalize 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) )
Temporal values
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)
More generally,
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
With this 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 switcher
or (<$>)
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 Functor
and Applicative
instances.
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
So 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
The inO2
and unO
functions from TypeCompose just manipulate newtype
wrappers.
See Prettier functions for wrapping and wrapping.
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 (:~>)
and (:->)
:
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 (:->.)
wrapping.
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.
Leave a comment