OK, this is probably a fairly controversial proposal, so I ask you to hear it out until the end before forming an opinion :)
Behavior and Discrete essentially represent the same thing: a time-varying value. I find the documentation's claim that Discrete is somehow inherently discrete dubious; apart from changes
, the interface of Discretes is identical to that of Behaviors. Which to choose basically comes down to
- Optimisation, and
- Interfacing with the outside world.
In my opinion, these concerns and their manifestation as Behavior vs. Discrete collide to make code less declarative, harder to structure, and harder to understand. So I went through working code using reactive-banana: the reactive-banana-wx examples.
In Arithmetic.hs, Behaviors are only used to poll the state of a text box in reaction to events.
In Asteroids.hs, Behaviors are used for the game state. bship
does not use Behaviors from the outside world, and I think it could be made Discrete without losing anything. brocks
uses a brandom
Behavior to poll the RNG from the outside world. However, it is only consulted in response to the etick
Event. (Incidentally, I'm not sure an RNG is a proper Behavior like this; the act of observing it changes it, and nothing else does (barring other threads accessing the global RNG too). That seems wrong to me.)
In CRUD.hs, Behavior is used to construct the DatabaseTime
record, which has an associated comment noting its similarity to Discrete. I believe DatabaseTime
could easily be rewritten in terms of Discrete. It is also used to poll the state of a text box.
In Counter.hs, no Behaviors are used.
In CurrencyConverter.hs, a single Behavior is used to poll the state of a text box in response to an Event.
In NetMonitor.hs, a Behavior is used to poll the state of the network in response to an Event.
In TicTacToe.hs, no Behaviors are used.
In TwoCounter.hs, a single Behavior is used to track the state of a toggle; it could easily be a Discrete instead.
In Wave.hs, no Behaviors are used.
Next I glanced at BlackBoard, which I could only find in the tree of commit b8c353b. It was too large for me to give it a really in-depth examination, but no use of Behaviors that don't fit in one of the above categories (poll cheap state, poll expensive state based on an event, or could easily be Discrete) immediately jumped out to me.
I took a brief glance at the only other public project using reactive-banana I know of, Jaek, but it was far too large for me to form an accurate impression. I suspect the majority or all of its Behaviors fit in one of the aforementioned categories too, though.
Now I'll address each category that I've shown Behaviors to be used for separately:
- Polling cheap state: There is probably little downside to using a Discrete here, as the GUI framework will already be updating its mutable state based on the flow of events that change it.
- Polling expensive state based on an event: This one is interesting! I am sceptical that all uses of this pattern are valid, like the RNG I mentioned, but certainly the network statistics example seems reasonable. Obviously, the polling of this state could be pushed inside the IO action an Event builds up, but this seems inelegant. Here's an idea:
fromPoll :: Event (IO a) -> NetworkDescription (Event a)
. You could, for instance, write brandom <- fromPoll $ (randomRIO (0,1) :: IO Double) <$ etick
, and then construct a Discrete
based on this. A useful combinator might be fromPollD :: Discrete (IO a) -> NetworkDescription (Discrete a)
. This does reduce modularity to a degree, but from the POV of interfacing with the real world, it's clearer (you control exactly when the possibly-expensive polls will occur), and I think such uses of Behavior are fairly rare anyway.
- Code that could easily use Discrete: Well, there's not much to say about this one :)
So, why use Behaviors instead of Discrete? Well, an obvious reason is that changes
exposes more than the abstract interface of a Behavior does. (One could make the same argument about initial
, but I don't think there's any reason not to offer this function for Behaviors, other than that as an "implementation detail", the computation of a Behavior can cause side-effects in reactive-banana.)
So here's my proposal:
- Remove the existing Behavior type.
- In the Implementation flavour, implement Behavior using Discrete. Replace
fromPoll
with the form I suggested above.
- Change the types of
initial
and changes
as follows:
initial :: Behavior a -> NetworkDescription a
changes :: Behavior a -> NetworkDescription (Event a)
- Document these (or at least
changes
) as being intended solely for the use of interfacing to the outside world.
- Reap the benefits of a simplified API, simplified implementation, and vastly simplified programming experience.
Note that I'm pretty sure that none of the above would involve any changes to the model; Discrete is a perfectly cromulent implementation of the Behavior type family.
Thanks for reading, and I'm interested in hearing your thoughts on this (for any value of you — if you're reading this bug report and use reactive-banana, please speak up!) :)