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
?
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 Word
s into one Event (Word, Word)
, which isn't really a trivial task for Event
s as they don't have an Applicative
instance like Behavior
s do. That's why I usually like to use Behavior
s in such cases, but as discussed before they don't get you further when you actually want to create the inputs. So if Behavior
s aren't the right solution here and Event
s tend to get tediously hard (or impossible in the worst cases) to combine, what is the right solution for this problem?
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:
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.