Applicative bots
In Functional reactive partner dancing, I mentioned that (a) the partially applied leading and following types have boilerplate Applicative
instances, and (b) the leading type corresponds to varying (reactive) values. Today I realized that those boilerplate instances are not very useful, and that they do not correspond to the Applicative
instance of Reactive
. In this post, I give a useful Applicative
instance that does correspond to the Reactive
instance. The instance definition is expressed in terms of the pair editor bot shown at the end of the “dancing” post, which seems to have a variety of applications.
The Applicative
instance has one awkward aspect that suggests a tweak to the formulation of leading. I give simplified versions of pair editing and Applicative
for the revised type. This change is in version 0.1 of the Bot libary.
Edit 2008-02-15: added FRP tags; prose tweak.
Oops
The multi-output versions of leading and following are formulated simply as the single-output versions, with a list-valued output type:
newtype a :-> b = Follows (Follow a [b])
newtype a :>- b = Leads (Lead a [b])
Partially applied, each of these types is a sort of composition of type constructors. For instance, (:->) a
is the type composition of Follow a
and []
. Since both of those type constructors are applicative functors, there are standard definitions of Functor
and Applicative
.
instance Functor ((:->) i) where
fmap f (Follows z) = Follows ((fmap.fmap) f z)
instance Applicative ((:->) i) where
pure x = Follows ((pure.pure) x)
Follows f <*> Follows x = Follows (liftA2 (<*>) f x)
and similarly for (:>-) i
.
In fact, these instance templates are abstracted into instances for the type composition operator (:.)
found in TypeCompose, so we can get the four instances for free if we define
type (:->) a = Follow a :. []
type (:>-) a = Lead a :. []
While the Functor
instances work fine, the rub (which I didn’t realize when writing the “dancing” post) is that the Applicative
instances are not what I want. They delegate to the Applicative
instances for Follow a
and for []
. The result is that each output of lf <*> lx
is the list of f x
for all f
in the (list-valued) lf
output at that point and all x
in the (list-valued) lx
output. In particular, the lf <*> lx
will have an output only when both lf
and lx
have simultaneous outputs.
Instead, I’d like lf <*> lx
to have an output whenever either lf
or lx
has an output. If lf
has an output f
, I want to output f x
, where x
is the most recent lx
output. Similarly, if lx
has an output, I want to output f x
, where f
is the most recent lf
output. This behavior is exactly how Applicative
works for Reactive
, as described in reactive-normal-form.
A solution and a problem
At the end of Functional reactive partner dancing, I showed a tool that is related to the desired Applicative
behavior.
editPairF :: (c,d) -> Either c d :-> (c,d)
Given an initial pair, editPairF
accepts replacements of either the first or second element and produces updated pairs, remembering the previous values. Since memory is involved, editPairL
is defined in terms of the generic accumulation combinator accumF
.
Let’s put editPairF
to work, to pair up two follows into a pair-valued follow. A new pair is output whenever either element gets output.
pairF :: (b,c) -> a:->b -> a:->c -> a:->(b,c)
pairF bc ab ac = (Left <$> ab) `mappend` (Right <$> ac) >>> editPairF bc
We had to supply the initial pair here, because follows don’t have initial values. Leads do, however, so the lead-pairing function has a simpler-looking type:
pairL :: a:>-b -> a:>-c -> a:>-(b,c)
The definition of pairL
works in terms of pairF
. The extra work involves disassembling and reassembling leads into and from initial values and follows.
ab `pairL` ac =
leads (liftA2 (,) bs cs) $ pairF (b,c) abf acf
where
(bs,abf) = splitL ab
(cs,acf) = splitL ac
-- Oh dear. b & c might not be well-defined
b = last bs
c = last cs
The awkward bit here is that, as formulated, a multi-lead (a :>- b
) could have multiple values even initially. For that reason, I (a) use a cross product (liftA2 (,)
) for the initial pairs, and (b) extract a single value from each lead to use in the pair-valued lead’s initial value. This second consideration is worse than awkward; it will fail if either initial value list is empty.
Is it really useful for a lead to have an initial list of values? Not that I know of. I allowed the flexibility because it made the type definitions so simple and uniform, which I’ve found has a decisive impact on the simplicity of code that works on the type.
Placing the initial-list problem aside for now, here is the simple and useful Applicative instance for leads. The pure
method makes a lead with an initial value an no future reponses. The (<*>)
method uses pairL
above to make a lead whose outputs are function/argument pairs, and then maps uncurried function application onto the pairs to get out the results.
instance Applicative ((:>-) i) where
pure x = leads [x] mempty
lf <*> lx = uncurry ($) <$> (lf `pairL` lx)
What about (:->)
(following)? I don’t think an Applicative
instance like the leading one can exist, due to lack of initial values. Perhaps there is an Applicative
instance corresponding to the one on Event (combining all pairs of occurrences over time).
A tweak, and we’re back to safe & elegant
The difficulty with pairL
comes from a feature of dubious value, namely having an initial list of values rather than exactly one initial value. Let’s consider removing that flexibility. Replace
newtype a :>- b = Leads (Lead a [b])
with
newtype a :>- b = Leads (b, a :-> b)
The pairing combinator for leads is now simpler. It’s also bomb-proof, since it has initial single values instead of lists:
pairL :: a:>-b -> a:>-c -> a:>-(b,c)
Leads (a,fa) `pairL` Leads (b,fb) =
Leads ((a,b), pairF (a,b) fa fb)
And we have simple and (I think) trouble-free instances:
instance Functor ((:>-) i) where
fmap f (Leads (b,fol)) = Leads (f b, fmap f fol)
instance Applicative ((:>-) i) where
pure x = Leads (x,mempty)
lf <*> lx = uncurry ($) <$> (lf `pairL` lx)
Leave a comment