reflex-frp / reflex

Interactive programs without callbacks or side-effects. Functional Reactive Programming (FRP) uses composable events and time-varying values to describe interactive systems as pure functions. Just like other pure functional code, functional reactive code is easier to get right on the first try, maintain, and reuse.
https://reflex-frp.org
BSD 3-Clause "New" or "Revised" License
1.06k stars 143 forks source link

`t` type parameter and time travel #89

Open tbenst opened 7 years ago

tbenst commented 7 years ago

Just started looking at Reflex and blown away thus far!

I've been looking for a FRP implementation to use in neuroscience, modeling electrode voltage as Behavior and action potentials (voltage "spike") as Events. After a spike occuring at time t1, it needs to be classified to a particular neuron based on the behavior of the electrode on the interval (t1-0.005,t1+0.005).

As I understand Conal Elliott's original specification, this is possible in classic FRP. Is there natural way to do time travel and intervals in Reflex? I'm a bit fuzzy on the timeline t in Reflex, but it seems like this is the place to implement time travel.

Thanks for making this library available!

ryantrinkle commented 7 years ago

Reflex doesn't currently implement any measure of time, although its interface is designed to be 100% compatible with some future extension adding that capability. The current Reflex semantics only demands (and provides) that "time" is totally ordered, i.e. that every occurrence of an Event is either before, simultaneous with, or after any other occurrence of any Event. Allowing the user to describe intervals of time would entail strengthening this structure from a total order to a metric space.

Supporting a metric on time is probably possible, but challenging. One problem is that lookback times need to be known in advance. For example, suppose that your program initially uses no lookback, but then suddenly decides to use 1 second of lookback. Where can we get that data? If we are garbage collecting correctly, it should have already been thrown away.

So, we will need some way of confining the use of lookback - perhaps a mandatory "ramp-up" whenever lookback is increased, combined with some kind of back-propagation that forces dependencies to retain a certain amount of history. This obviously complicates the semantics, but hopefully something elegant can be achieved nonetheless.

Currently, in Reflex, the t parameter designates a "Timeline", which is a slightly more general concept than a "Time" type. In particular, it's possible to switch implementations of Reflex by changing the t parameter; this is how the test suite checks that Spider and Pure agree with each other. One benefit of this system is that it allows the construction of "Reflex transformers", which I believe could prove useful in the implementation of something like measured time. By writing it as a transformer rather than a separate implementation, a lot of the headaches of basic FRP implementation can probably be saved.

tbenst commented 7 years ago

Where can we get that data? If we are garbage collecting correctly, it should have already been thrown away.

For pure behaviors as a function of time, the computation could be done as pulled. In my case, all impure behaviors of t are recorded to disk as they come in, simplifying lookback significantly. Retaining total history makes lookback much easier, and partial history seems inherently application-specific, so perhaps it's opt-in by defining a lookback method.

I very much like the idea of multiple timelines, neat.

ryantrinkle commented 7 years ago

Yep, if you keep the whole history, things are a lot simpler! However, it can be tricky to actually do that. Firstly, you do still need to deal with application startup/deployment/etc. - the only time you don't have lookback. Secondly, there are some situations where you may have trouble serializing values - e.g., what about a Behavior t (a -> b)? You might be able to deal with this by restricting MonadHold to only allow holding serializable data, or perhaps you could use a CloudHaskell-style approach, which could allow you to serialize closures and such.

Another potential problem with infinite lookback would be CPU usage. E.g., suppose you construct a brand-new "fold through time", e.g. a running sum of an Event. If that hasn't been getting computed all along, you'll have a ton of catch-up work to do to get it up to the current time.