data Event t a data Behavior t a instance Functor (Event t) instance Functor (Behavior t) instance Monoid (Event t) instance Applicative (Behavior t) filterE :: (a -> Bool) -> Event t a -> Event t a accumE :: a -> Event t (a -> a) -> Event t a stepper :: a -> Event t a -> Behavior t a
I have omitted never and union, because they are just mempty and mappend from Monoid. I have also omitted accumB, because my understanding is that accumB x e is just a more efficient version of stepper x (accumE x e), and I am not concerned at all about efficiency yet.
Interactivity
So, how do I implement this interface? Could I represent Event as a map from timestamp to values, for example? It sounds like this representation would admit an implementation for all the required functions, but that would not allow me to handle events as they come, like I can do with gloss-banana. I would have to open the window, and let the user click around without being able to update the display. After accumulating all the mouse events, I would gather them into one big map, an Event t InputEvent, from which I could construct an Event t Picture. At this point, I would know exactly what I should have displayed after each of the mouse clicks, but it would be too late for that. Clearly, I need to find a way to obtain the early outputs before I learn about the later inputs...
With a list instead of a map, I can think of at least two laziness-based approaches for this kind of problem, but I'm not ready for a clever implementation yet. Let's keep things simple!
I said that a map-based representation would not allow me to handle events as they come. So clearly, being able to handle events as they come should be part of my API. What would a function for handling a new event look like?
handleInputEvent :: InputEvent -> Event t a -> IO (Maybe a)
I give the next InputEvent, the Event decides what it wants to do with it, and optionally triggers an event of type a based on it. But it's weird to have such a concrete type as InputEvent; surely reactive-banana isn't aware of the specifics of the event types supported by gloss. Event should have a type parameter indicating which type of input events it expects. Oh! Maybe that's what the t is for?
handleEvent :: t -> Event t a -> IO (Maybe a)
Much better. You might wonder why I am returning an IO action instead of a pure Maybe a: it's because of accumE. Accumulating the values of the events as they come requires storing an intermediate state somewhere, and a pure function would not be able to store its new state anywhere.
Well, I don't want to be stuck inside IO yet, so let's thread this state explicitly:
handleEvent :: t -> Event t a -> (Maybe a, Event t a)
I give the next t-valued input event, the Event decides what it wants to do with it, and optionally triggers an event of type a. The Event might have changed its internal state somehow, so it also returns a modified copy of itself, one which is ready to receive the next event.
Event
I now have a much clearer idea of the way in which I am going to use (or "eliminate", in type-theory-speak) values of type Event, but I'm no closer to figuring out how to represent those Event values. Or am I? When a type has a single (or a most general) eliminator, we can just use that eliminator as a representation:
data Event t a = Event { runEvent :: t -> (Maybe a, Event t a) } deriving Functor handleEvent = flip runEvent
Is this representation good enough? Can the rest of the API be implemented on top of it? As I get stuck trying to implement union, I remember that one input event can cause more than one output event to be emitted at the same time. I make a small adjustment:
data Event t a = Event { runEvent :: t -> ([a], Event t a) } deriving Functor
With this representation, I can easily derive an implementation for the first few combinators, simply by following the types. A good sign!
instance Monoid (Event t a) where mempty = Event (const ([], mempty)) e1 `mappend` e2 = Event go where go t = (xs1 ++ xs2, e1' <> e2') where (xs1, e1') = runEvent e1 t (xs2, e2') = runEvent e2 t filterE :: (a -> Bool) -> Event t a -> Event t a filterE p e = Event go where go t = (filter p xs, filterE p e') where (xs, e') = runEvent e t
Interlude: on the technique known as "follow the types"
Following the types is a technique which allows a function to be implemented very quickly, by taking decisions based on the types of the values involved instead of their meaning. Since it happens so quickly, it's a process which is a bit difficult to describe, as stopping in the middle of it in order to take notes will break the magic. Here is an attempt at reconstructing the magic after the fact.
First, let's dispel a possible misconception: I'm not a human version of djinn. That is, when I "follow the types", I do not blindly pick a random expression of the expected type, hoping it will be correct. Instead, I use the types as a shortcut when there is only one obvious way to go forward, and otherwise I pick an expression which fits the immediate context.
For example, in the code for filterE, I begin with the Event constructor, I pass it an intermediate function receiving a value of type t, and I eliminate the e argument using its only eliminator and the only value of type t I have on hand. I did not waste any time on those uninteresting details, because there are simply no alternatives to consider.
The call to filter p xs, on the other hand, is deliberate: a simple xs would have had the same type, but would have been incorrect. Yet the reason I used filter was not because I stopped to think about correctness; that would have involved reasoning based on the meaning of the values. Instead, I knew from the start that I wanted to filter out values of type a. It is the context of implementing an event-filtering function which guided me through the non-forced parts of the implementation: as I was following the types, I was on the lookout for values of type a or [a], ready to filter them on sight. This kind of context is also the reason why I used [] in the definition of mempty, versus (++) in the definition of mappend.
A very different example of context is that of the recursive call to filterE p e', where e and e' would have also type-checked. This part of the implementation is in a recursive position for Event t a, just like the tail is a recursive position for lists. In such a recursive context, I naturally picked a recursive expression, of the proper type of course, and using smaller arguments where possible.
Simplification
After a type-based implementation, I like to take a moment to go beyond the types and examine the meaning of all the values involved. Reading the code I just blindly wrote for never/mempty, the Event in which no event ever occurs, I see that the first input event is ignored, and that no output events are produced in response. Afterwards, recursively, no event ever occurs, and that's exactly what I want.
This reviewing phase is also a good opportunity for simplifications, such as replacing (\_ -> ...) with const or rearranging the pieces in a pointfree style. One simplification opportunity I notice is that [] is the mempty for lists, and (mempty, mempty) is the mempty for pairs, and const mempty is the mempty for functions. This, and a similar chain for mappend, allows the Monoid implementation to be simplified greatly:
instance Monoid (Event t a) where mempty = Event mempty Event e1 `mappend` Event e2 = Event (e1 `mappend` e2)
In this version, all the details are implicit, so much so that it's hard to follow the meaning of the values. But it's also a much more elegant implementation, and I've just read and understood the expanded-out version anyway, so I'm confident that the meaning is what I expect.
Behavior
The workflow for Behavior and its combinators is very similar to Event, so let's go through it quickly.
How do I represent/eliminate a Behavior? Probably the same way I eliminate events, except that behaviours have a value at every point in time, so I should receive exactly one a instead of a (potentially-empty) list of them.
data Behavior t a = Behavior { runBehavior :: t -> (a, Behavior t a) } deriving Functor
Trying to implement stepper, I realize that a behaviour is also supposed to hold a value before the very first event, and also between events. I need a second eliminator:
currentValue :: Behavior t a -> a
Now I have two eliminators. What would be a most general eliminator, from which the two others could be implemented?
generalEliminator :: Behavior t a -> (a, t -> Behavior t a)
I no longer need an a on the right-hand side of the (t -> ...), because currentValue can extract this a from the returned behaviour. I transform this most general eliminator into a representation for Behavior, giving convenient names to the pair's two components:
data Behavior t a = Behavior { currentValue :: a , runBehavior :: t -> Behavior t a } deriving Functor
Following the types is very straightforward this time.
instance Applicative (Behavior t) where pure x = Behavior x fx where fx _ = pure x Behavior f ff <*> Behavior x fx = Behavior (f x) fy where fy t = ff t <*> fx t
As before, this can be simplified: (\_ -> x) is the pure x for functions, and thus fx could be written as pure (pure x). Similarly, fy could be written by nesting two (<*>), whatever that means. This hints at a variant implementation of Behavior based on Compose, which will hide the details of the nesting:
data Behavior t a = Behavior { currentValue :: a , runBehavior :: Compose ((->) t) (Behavior t) a } deriving Functor instance Applicative (Behavior t) where pure x = Behavior x (pure x) Behavior f cf <*> Behavior x cx = Behavior (f x) (cf <*> cx)
I'm not convinced that this variant is actually better, because the other combinators don't benefit from Compose: they just peel off the Compose layer, do their work, and put it back on. Speaking of the other combinators, I've skipped two of them in my exposition. I left them for the end, because...
When following the types leads nowhere
The last two combinators don't fit the mantra of following the types very well. Here is part of accumE:
accumE :: a -> Event t (a -> a) -> Event t a accumE x e = Event go where go t = (_xs, ...) where (fs, e') = runEvent e t
The next step is to find an expression of type [a] for _xs. Unused values include fs, of type [a -> a], and x, of type a. One obvious and type-correct way to combine those two values is to apply all the functions in the list to x:
_xs = fmap ($ x) fs
But we're in an accumulator context, so I'm expecting something like a fold instead. When something doesn't seem right, it's best to stop following the types and switch to "figure out what I'm trying to do" mode.
In this case, what I am trying to do is to thread the x through all the functions, returning all the intermediate values. It turns out there is a standard library function for that, scanl:
accumE :: a -> Event t (a -> a) -> Event t a accumE x e = Event go where go t = (xs', accumE x' e') where (fs, e') = runEvent e t xs = scanl (flip ($)) x fs x' = last xs xs' = tail xs -- skip the initial unmodified x
If I was more familiar with scanl, I could have used it without thinking, remaining in follow-the-types mode. But then I wouldn't have thought to skip scanl's initial copy of x, I might have recurred with x instead of last xs, and everything would have been completely wrong.
My implementation for the last combinator is also quite wrong:
stepper :: a -> Event t a -> Behavior t a stepper x e = Behavior x (Compose go) where go t = stepper x e' where (xs, e') = runEvent e t
I did not use xs, and as a result, I recur on the wrong x. Here is what I should have written instead:
stepper :: a -> Event t a -> Behavior t a stepper x e = Behavior x (Compose go) where go t = stepper (last (x:xs)) e' where (xs, e') = runEvent e t
The only trick here is that I prepend x to xs in order to ensure that the argument to last is non-empty.
Conclusion
Following the types didn't work for every single combinator, but when it did work, it quickly produced a correct implementation on the first attempt.
Yup, it's working! It even has the same leak-until-clicked behaviour as the original :) |
When it didn't work, following the types did not lead to any wasted work. For ease of presentation, I have shown an incorrect type-based version followed by a corrected meaning-based version, but during my original implementation, there wasn't a sharp break between the two phases. Like I said, it all happens very quickly, so it's hard to describe exactly what is going on inside my head when I code. I think I always focus on the correctness of what I write. It's just that over time, I have learned that when the types which are in scope are sufficiently distinct, it's okay for "correct" to mean simply "type-correct". Whereas at other times, perhaps mere seconds later, it becomes important to understand what the values actually mean.
If you came here to learn how to follow the types yourself, and you're disappointed by my vague explanations, don't be. I did not set out to learn to follow the types; I just wrote lots of Haskell code, and my brain learned to focus on the types when appropriate. You'll get there, don't worry.