With ideal FRP semanitcs, a behavior is a function over time: Time → a
—it can be changing continuously over time. This means that calling an IO action on every change is not semantically sound because there is no notion of a discrete change in the semantics.
This also makes sense in practice: depending on when the value of a behavior changes is often too implementation-dependent. Consider, for example, the mouse position: how often the value changes is based on how often it's polled, which is very system dependent. Even if the actual behavior is not continuous, the discrete changes are still a detail that we do not want to leak into the your program. (In an ideal world, perhaps that behavior would be continuous too—maybe the system would do some sort of interpolation or smoothing to compensate for the underlying polling in hardware.)
To avoid depending on these arbitrary implementation details, you have to be explicit about when to sample your behavior to fire your IO action. You can do this by taking the time component of another event stream, creating a new event stream with events at the same time but values based on the current value of the behavior. In Reactive Banana, you can do this with the <@
operator. Specialized to our types, we get:
(<@) :: Behavior a -> Event b -> Event a
In essence, we map the value of the behavior over an event stream, giving us a new event stream. Then we can just use a standard function to fire off an IO action on every event.
The final question, then, is where you get your event stream from, which will depend on your specific program. If the behavior changes because of some specific user action, you can get a stream for that action. You could also just create a timer event and poll at a given time interval. Or you can even just do both, using union
! A nice illustration of how FRP is pretty composable.