Search code examples
haskellreactive-banana

Dynamic inputs from a Behavior in reactive-banana


When I want to work with dynamic inputs in reactive-banana, I usually envision a system that works roughly like this:

data InputSpecification -- Some data structure that specifies the inputs
                        -- which should be active right now

data MyInterestingData -- Some data that is relevant to my business logic
                       -- and is gathered through the dynamic inputs.

emptyData :: MyInterestingData
emptyData = -- Some initial MyInterestingData

setupDynamicInputs :: Event (InputSpecification) -> MomentIO (Behavior MyInterestingData)
setupDynamicInputs specE = do
    newBehavior <- execute $ updateDynamicInputs <$> specE
    switchB emptyData newBehavior

updateDynamicInputs :: InputSpecification -> MomentIO (Behavior MyInterestingData)
updateDynamicInputs = -- Here the dynamic inputs are set up according to
                      -- the specification and set up to update the returned
                      -- Behavior

This works quite nicely and the inputs are updated whenever a new InputSpecification is fired.

A problem I often stumble across is that my InputSpecification doesn't come in an Event but rather as a Behavior InputSpecification (probably because I needed Applicative combinators to construct it). The above approach doesn't work then, as execute and switchB can't be used on Behaviors.

As a simple solution, I could use this function from the reactive-banana documentation:

plainChanges :: Behavior a -> MomentIO (Event a)
plainChanges b = do
    (e, handle) <- newEvent
    eb <- changes b
    reactimate' $ (fmap handle) <$> eb
    return e

Then I could just use setupDynamicInputs on the Event obtained from plainChanges, but:

However, this approach is not recommended[...]

So I'm a bit reluctant to use this approach.

Is there a "cleaner" approach to keep my inputs in sync with the specification, when the specification is kept in a Behavior?

Edit

As Heinrich Apfelmus pointed out in his answer, the solution to my original question is not to use a Behavior for updating the InputSpecification. While I can understand the reasoning behind that, it doesn't solve the problem I'm having, so I will try to explain why I wanted to use a Behavior here.

Updating the input through an Event is easy as long as the input is specified by a single input. As an example, if the dynamic inputs consists of a sequence of inputs, the specification for those inputs would just be an non-negative integer that denotes how many inputs should be shown.

It gets more complex once the input specification is obtained via more than one input. For example, let's say our InputSpecification becomes (Word, Word) and specifies a grid of input with the given dimensions. If I obtain those dimensions via two different inputs, I would have to combine two Event Words into one Event (Word, Word), which isn't really a trivial task for Events as they don't have an Applicative instance like Behaviors do. That's why I usually like to use Behaviors in such cases, but as discussed before they don't get you further when you actually want to create the inputs. So if Behaviors aren't the right solution here and Events tend to get tediously hard (or impossible in the worst cases) to combine, what is the right solution for this problem?


Solution

  • Well, one reason why this may not work is that it, perhaps, should not work. There is a litmus test for determining whether modeling the situation with a Behavior makes sense: What happens if Behavior InputSpecification were a genuinely continuous function? Say, you had a continuous frequency range (e.g. for radio stations) and each frequency would be associated to a new input that has to be set up. If you were to do a continuous frequency sweep, then you would have to create and discard infinitely many inputs, which is not possible. This is an indication that there is a deeper reason behind Event InputSpecification being the right type.

    More generally, the Behavior type encapsulates two important invariants:

    1. It does not depend on a sample rate.
    2. You cannot detect when or how often it "changes". For instance, if you have an Event whose occurrences all have the same value [(0 seconds, x), (2 seconds, x), ..], then this invariant says that applying stepper to this will yield a Behavior that is indistinguishable from pure x.

    For pragmatic reasons, invariant 2 can be circumvented with changes function. You are allowed to use it if you feel that it "morally preserves the invariant". For instance, you can use it to display a text value on the screen only when the Behavior changes; this is more efficient than, say, polling at a fixed sample rate. Since the visual end result is the same for either Behavior mentioned above, you morally preserve the invariant in this case.


    EDIT:

    It looks like you need more explicit control about when to update. In this case, you can use an explicit event e :: Event () that keeps track of when the inputs should be updated. Then, you can use the following combination to update the inputs only when this event triggers

    e2 <- plainChanges (imposeChanges b e)
    execute $ updateDynamicInputs <$> e2
    ...
    

    (There should be a pure alternative for this, I will look into this.)

    Alternatively, you can replicate a "Behavior with notification for updates" machinery by hand, for instance introducing a type

    data Dynamic a = D (Behavior a) (Event a)
    

    and implement Applicative etc instances for this. This is a bit heavyweight but may be just what you need.