"Behavior now" in FRP

427 views Asked by At

In a previous SO question (Is it possible?: Behavior t [Behavior t a] -> Behavior t [a]) we were analyzing the existence of a Behavior join (to use reactive-banana terms).

Behavior t (Behavior t a) -> Behavior t a

Implemented in the semantic model as follows

type Behavior t a = t -> a

behaviorNow :: Behavior t (Behavior t a) -> Behavior t a
behaviorNow f t = f t t

While implementing this directly would be unfortunate since we could produce a Behavior Monad using const and behaviorNow, if and how does behaviorNow violate the semantics of FRP?

I'd love to hear answers using the terminology of any other FRP system along with comparisons if meaningful.

2

There are 2 answers

0
Cirdec On

In a poll based FRP system, any behavior has a meaningful join

  • the sample of join bb is the sample of the b obtained by sampling bb

In push based FRP, any behavior that is a step function composed with other step functions has a meaningful >>= and join. Pushing values through >>= can be described in imperative terms:

  • when the argument of the bind changes, evaluate the bind and
    • change the current step function to the returned step function
    • change the value to the value of the current step function
  • when the value of the current step function changes, change the value

Providing a Monad instance may be slightly undesirable because it is likely to be chosen by preference by library users, even if it is less efficient. For example, the code in this unrelated answer performs more work when a computation was built with >>= than if it had been equivalently built with <*>.

Conal Elliott described in declarative terms a join for simultaneously pushing and polling values for behaviors built from step functions:

-- Reactive is a behavior that can only be a step function
data Reactive a = a `Stepper` Event a
newtype Event a = Ev (Future (Reactive a))

join :: Reactive (Reactive a) -> Reactive a
join ((a `Stepper` Ev ur) `Stepper` Ev urr ) =
    ((`switcher` Ev urr ) <$> ur) _+_ (join <$> urr )

switcher :: Reactive a -> Event (Reactive a) -> Reactive a
r `switcher` er = join (r `Stepper` er)

where Future is the type for a value we haven't seen yet, _+_ is the first of the two Future possibilities to occur, and <$> is infix fmap on Futures. [1]

If we don't provide any other means of creating behaviors than

  • the constant function (which is trivially a step function)
  • a "stepper" that remembers the most recent value of an event
  • application of various combinators of behaviors where the combinators themselves aren't time-varying

then every behavior is a step function and we can use this or a similar Monad instance for behaviors.

Difficulties only arise when we want to have behaviors that are continuous or are a function of some time other than when an event occurred. Consider if we had the following

time :: Behavior t t

which is the behavior that tracks the current time. A Monad instance for polling the system would still be the same, but we can no longer push changes through the system reliably. What happens when we make something as simple as time >>= \x -> if am x then return 0 else return 1 (where am t is true for times in the morning)? Neither our definition of >>= above nor Elliot's join can admit the optimization of knowing when the time changes; it changes continuously. The best we could do to >>= is something like:

  • if we know that the argument to the bind is step valued then
    • when the argument of the bind changes, evaluate the bind and
      • change the current step function to the returned step function
      • change the value to the value of the current step function
    • when the value of the current step function changes, change the value
  • otherwise
    • return an abstract syntax tree for this >>=

For the join form, we would be reduced to doing something similar, and simply record the AST in the instance that the outer behavior in a join isn't a step function.

Additionally, anything built using this as an input could change at noon and midnight, whether or not any other event was raised. It would taint everything from that point on with the inability to reliably push events.

From an implementation point of view, our best option would seem to be to continuously poll time, and replace anywhere it was used with a stepper built from the polling events. This wouldn't update values between events, so now users of our library can't reliably poll values.

Our best choice for an implementation would be to keep an abstract syntax tree of what happened with arbitrary behaviors like these and provide no means to generate events from behaviors. Then behaviors can be polled, but no updates will ever be pushed for them. In that case, we might as well leave it out of the library, and let the user pass around ASTs (which they can get for Free), and let the user evaluate the entire AST every time it's polled. We can't optimize it any more for the library user, since any value like this can change continuously.

There is one final option, but it involves introducing quite a bit of complexity. Introduce the notion of predictability for properties of continuously varying values and computations of continuously varying values. This would allow us to provide a Monad interface for a larger subset of time-varying behaviors, but not for all of them. This complexity is already desirable in other parts of programs, but I don't know of any libraries outside symbolic math which attempt to address this.

0
Heinrich Apfelmus On

(Author here.)

First note, that the behaviorNow function is the monadic join.

In reactive-banana-0.7, Behavior t is not a monad beause that would have serious consequences for efficiency.

The first and most important problem is that behaviors can also be stateful. In conjunction with join, this would lead to time leaks. The main indication of problems is that the starting time t of the inner Behavior t is the same as the outer one. For instance, consider the program

e  :: Event t Int
b  :: Int -> Behavior t Int
b x = accumB 0 $ (x+) <$ e

bb :: Behavior t (Behavior t Int)
bb = stepper (pure 0) $ b <$> e

The behavior join bb would need to keep track of the whole history of the event e in order to perform the accumulation in the definition of b. In other words, the event e could never be garbage collected -- a time leak.

A second problem is that internally, the implementation of Behavior t also includes an event that keeps track of when the behavior changes. However, a liberal use of the join combinator, for instance as implied by do notation, would lead to rather convoluted calculations to determine whether the behavior has changed or not. This is contrary to the reason for keeping track in the first place: efficiency by avoiding expensive calculations.


The Reactive.Banana.Switch module offers various combinators that are cousins of the join function, but avoid the first problem with cleverly chosen types. In particular:

  • The switchB function is the most direct analogue of join.
  • The AnyMoment Identity type is similar to the Behavior type, but without state and without keeping track of changes. Consequently, it has a monad instance.