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