Simply efficient functional reactivity
I submitted a paper Simply efficient functional reactivity to ICFP 2008.
Abstract:
Functional reactive programming (FRP) has simple and powerful semantics, but has resisted efficient implementation. In particular, most past implementations have used demand-driven sampling, which accommodates FRP’s continuous time semantics and fits well with the nature of functional programming. Consequently, values are wastefully recomputed even when inputs don’t change, and reaction latency can be as high as the sampling period.
This paper presents a way to implement FRP that combines data- and demand-driven evaluation, in which values are recomputed only when necessary, and reactions are nearly instantaneous. The implementation is rooted in a new simple formulation of FRP and its semantics and so is easy to understand and reason about.
On the road to efficiency and simplicity, we’ll meet some old friends (monoids, functors, applicative functors, monads, morphisms, and improving values) and make some new friends (functional future values, reactive normal form, and concurrent “unambiguous choice”).
Cale Gibbard:
In your definition of ‘before’ at the top of page 4, it appears that the parameters are swapped.
5 April 2008, 12:18 amconal:
Thanks, Cale. I’ve added an errata section to the paper’s web page.
5 April 2008, 8:15 amanonymous:
Great paper! Small typos: On page 8, in the definition of adjustTop, the constructors should be named “Ev” resp. “Fut”, not “Event” resp. “Future”. Also, on page 3, in the semantic Monoid instance definition for Event, the “O” constructor should either not be used, or be introduced earlier.
5 April 2008, 1:10 pmMax Bolingbroke:
Just finished reading your paper Conal: great to see that FRP is still getting some love! I did notice some minor things which may be errors: – In the newtype declation for Future, I’m don’t think that FTime should have a “t” after it – On page 6, I think it says “Applicative Behavior” where it should say “Applicative Reactive” – On page 7, I think “will have to block the” should read “will have to block until the” – Similarly on pagc 8, “do some work based partial applications” should be “do some work based upon partial applications” – The Monad instance for Events in section 7.2.2 says “instance Monad Reactive” at the top of it – On page 9 “image to display window” should probably read “image to a display window” – In the conclusion on page 12, “paper allows retains” should probably just read “paper retains”
Am I right in thinking that because of the way the Applicative instance for Reactive is defined, if we have a reactive function like “f e1 e2″ and e1 changes, e2 will be re-sampled? Does this have performance implications? I suspect that this doesn’t actually matter, but I don’t have it totally straight in my head yet.
One thing I would like to see incorporated into FRP systems is a notion of diffs, so if e.g. an underlying data structure is modified in just one place (imagine a user pasting into the middle of a text buffer) the system can use the diff to efficiently recompute outputs that depend on that data structure (e.g. just adding the length of the inserted text onto the current cursor position rather than working it out from scratch). I don’t what machinery would be required to make this work, but I feel it would involve zippers to let you recover diffs in a principled way.
A use case I have in mind for this is modelling a build system as a FRP system where the files (e.g. java source files) are what vary over time and outputs (e.g. class files) are the result of applying the “javac” function to the files. The difficulty here is that the javac compiler can avoid recomputing some things if it has the OLD class files as input. So you get something similar to this:
class_files = javac (source_files :: [JavaSource]) (class_files :: [JavaClass])
So you need some way for FRP to tie the knot by feeding back in the cached previous value or in general an actual diff.
This is pretty ill-informed speculation, as I’m sure you can see, but maybe you can recover something useful from my rambling
6 April 2008, 9:40 amconal:
Max,
Thanks very much for catching these mistakes. I’ve updated the errata.
(I’ve changed your variable names.) I think you want to know, given
f<$>r1<*>r2
, whether the value ofr2
will get recomputed whenr1
changes). (By the way,(<$>)
and(<*>)
are left-associative.) No, it won’t (nor vice versa), because reactive values have an all-data, no-function representation, and so we get the usual caching property for data structures. Just like lists: the head gets computed the first time you look but reused the second time. This lovely caching property is what I was trying to get at in the end of section 7.1.2.I’ve been tinkering with this idea as well. I haven’t yet seen how to make it simple, general and useful. It’ll come, I think.
Yes! Purely functional
make
is one of the applications I really want to tackle. I think we can do a much better job of correctness and efficiency than a standard build system can, and with no redundant specification (code + makefile) to boot. I have a partial prototype on the back burner.I don’t think you’d want knot-tying exactly in your example (assuming you mean
classes = javac<$>sources<*>classes
), since the knot would be at each moment, while you want the new output to be influcenced by a previous output. In that case, one of the accumulator combinators (Section 12) would probably do the trick.Better yet, I’d like to open up the structure of source code, compilers (linkers, etc), and object code and embed FRP throughout, to bring much more incrementality to compilation (etc).
6 April 2008, 12:32 pmTom Davies:
Hi Conal,
Is there any source code available for the concepts discussed in this paper? As a relative FP neophyte I find papers very terse and can usually understand better by looking at more comprehensive code as I read them.
Off topic, I watched on youtube the interesting tangible values talk you gave at Google. Another visual FP environment is Business Object’s ‘Gem Cutter’ (http://openquark.org/Open_Quark/Welcome.html).
It is a conventional boxes and lines model, but you can ‘burn’ one of the inputs to a function, which converts its output from a value to a function, allowing you to connect the box to a function input on another function.
7 April 2008, 1:56 pmconal:
Hi Tom,
I haven’t released my library yet, as it’s a bit unstable at the moment, and I want to study & tune the performance. I then want to hook it up to functional GUI systems like Phooey and TV/GuiTV, as well as 2D and 3D animation, as in Fran. If you watch the Hackage What’s New feed, you’ll see these libraries appear. Or try Phooey & GuiTV now, which are both implemented on top of an earlier version of the code described in the paper (as well as wxHaskell).
7 April 2008, 2:54 pmConal Elliott » Blog Archive » Simplifying semantics with type class morphisms:
[…] About « Simply efficient functional reactivity […]
8 April 2008, 8:22 pmRaoul Duke:
Re: “Yes! Purely functional make is one of the applications I really want to tackle.”
That would be very interesting to see. Coming from a completely different angle, more of a software engineering perspective, I would like to be able to use interesting properties I’d hope might be obtained from the purely functional approach.
http://therightabstractions.wikispaces.com/BSL
10 April 2008, 5:08 pmRyan Ingram:
Overall this is a really fun to read paper, and it seems to build in very clear steps towards a neat result. But there’s one thing that seems missing: how do you implement “compare” for future times? Do you have some sort of global “what time is it?” variable that you check to know for sure that one future won’t be filled with the same value as the other?
Did I overlook something obvious?
18 April 2008, 11:31 amconal:
Hi Ryan. I’m glad you liked the paper. I might not be understanding your question.
18 April 2008, 11:58 am(<=)
on future times, and on improving values in general, is described in section 10 and figure 3, usingunamb
,compareI
, andexact
. If you really wantcompare
instead of(<=)
, the definition would be very similar to(<=)
from section 10.Ryan Ingram:
Right, I got all that. The steps I see in the paper:
1) “dumb” improving values which contain a large number of “non-occurrence” events. 2) make implementation better assuming the existence of compareI 3) ?? somehow compareI gets implemented ?? 4) Profit!
So step 3 is the bit I feel is missing. How can you implement compareI efficiently without some way to know “time X has passed! I know I won’t get filled before time X”?
18 April 2008, 3:02 pmRyan Ingram:
To be clear, there is one implementation of compareI in the paper, for “exactly”; but I was thinking about how to use the system for a real application, where events are generated by an external source. In such a system you would have to define something like the following:
But what goes in the ? above?
18 April 2008, 3:23 pmconal:
Ryan: Now I get your question: how to implement
compareI
on primitive externally-based events (rather thanexactly
and those made bymin
andmax
). Yes, I did leave that bit out of the paper. Here’s one answer.Let
ta
be the (initially unknown) value of the primitive time. Then to compare the future time to a timet
, tryta `compare` t
, but that tactic will hang untilta
is known, possibly long aftert
passes. We’ll get the right answer (unlessta == infinity
), but perhaps much too late. So, make a second concurrent test that waits until it can proveta > t
(without knowingta
). How can we make this last test? Create a new IVar (write-once synchronization variable), which will be filled in withta
whenta
is known (if ever). Thenwhere
assuming
is as defined in the paper (figure 4 and section 11). I’m treatingreadIVar
as pure, since it can yield only one value.I’m assuming that
18 April 2008, 4:39 pmwaitFor t
waits not just until timet
, but until all of external futures (occurrences of external events) at or beforet
to have been captured (had their times written to their respective IVars). This assumption is necessary to justify the use ofunsafePerformIO
and the correctness ofimpT
.Ryan Ingram:
A simplifying assumption that seems to remove a lot of the complexity involved here:
Given
b :: B a
, andt :: Time
,(b t)
is not allowed to depend on events that occur at time > t.Then, while evaluating b t, we can assume that any events that occur at or before t are safe to evaluate and access directly, and any event that happens after time t can be ignored.
Then you can implement future times with the following interface:
which takes the current time and returns Nothing if an event hasn’t happened yet, or Just (actualTime, a) if the event has happened, with the invariant that when it is evaluated, the time passed in must be at or before the current time.
Deterministic futures (like exactly) are trivial to implement with this interface, and min/max (for “merge”) also seems simple. You can set up input from external sources pretty easily as well:
This requires a similar rule as your “waitFor” which is that all IVars for futures at time t must be filled before evaluating
(b t)
. But this exactly corresponds to the usual imperative loop: read input, evaluate state, render output.If I’m correct, this would allow you to go back to a simpler, single threaded execution model and avoid potential non-determinism and the need for unamb entirely.
Also, a question/comment: does this implementation of “race” work (without the extra MVar):
I think you might still end up with double kill (takeMVar frees both threads to kill each other) but it doesn’t matter because they are both immediately exiting after the kill anyways.
18 April 2008, 5:29 pmconal:
Hi Ryan — I’m glad to see this work is engaging your interest!
First, your simplifying assumption is indeed guaranteed (or preserved) by the semantics of the behavior constructs in the paper, so no problem there. (Even a bit stronger:
b t
doesn’t depend on occurrences att
.)Second, about
race
, maybe so. I don’t trust my judgment about imperative concurrency.On to idea about
Future
:My main goal in this work is data-driven (event-driven) evaluation, in contrast with the previous pure FRP implementations, which are demand-driven. I believe the representation of
Future
you suggest would work fine and has a simple connection with the semantic model. And I don’t know how to use it in a data-driven implementation. Demand-driven implementations are simple enough that I probably wouldn’t bother introducing theFuture
abstraction.While reactive normal form came to me in a flash, ensuring determinism was the trickiest piece of the puzzle for me. I like the idea of finding of a simpler determinacy solution than mine. And I don’t have a compelling reason to believe that concurrency is necessary for pure, data-driven (determinate) FRP. While FRP semantics is highly concurrent, the implementation concurrency here (in
18 April 2008, 6:47 pmunamb
) doesn’t seem to be there for the semantic concurrency.Ryan Ingram:
I think I get it now. And I do think some form of concurrency is necessary for all but the trivial case; consider, for example, purely functional make where the main function is of the type
Clearly there has to be someone processing
occs $ make $ getFsEvents
in order to spawn commands, and also clearly there has to be someone generating the filesystem events, and those can’t be running on the same thread; occs is going to block until a new command comes in, so no filesystem events could get generated.Now, whether that means you need concurrency in the innards, I’m not sure. But it seems likely, or at least some way of polling all the event sources in the system.
I have an idea for a more elegant implementation using slightly modified types for B and E; I’ll see if I can put it together & let you know how it turns out.
Thanks for the interesting ideas & food for thought!
18 April 2008, 9:05 pmRyan Ingram:
Here’s a new representation for Future that gives you the data-driven behavior you want:
The main inspiration I had was that each future can live in a different universe, with its own concept of time. mappend on futures represents entwining two universes’ timelines together. Using STM’s “retry” and “orElse” combinators, we can wait for multiple universes simultaneously & efficiently in a single thread.
There’s an article on my blog that goes into more detail.
20 April 2008, 6:27 pmconal:
Ryan: I read your post and have some questions:
Future a
type have the same denotation as in my paper, namely(Time,a)
? DoesfirstFuture
have the same semantics as mymappend
(picking the earlier future)?waitFor
? Related: what’s the semantics offirstFuture
, and what do its calls towaitFor
accomplish? I see the comment “start over if something changes in the meantime”, but I don’t see how the waiting influences the returned value, so I don’t know how you could get determinate semantics. DoeswaitFor
sometimesretry
?fmap
and(<*>)
on yourFuture
representation, and have you checked whether it re-applies functions each time the result is accessed? My first implementation used STM in a similar way, and I realized that the obvious (to me)fmap
and(<*>)
implementations failed to cache their values. My fix (and Jake McArthur’s–see comments on your post), used a writer thread.Ryan Ingram:
Hi Conal, hopefully I can answer your questions satisfactorily.
(1) Absolutely: that was the goal. In particular, here is the implementation of “force”:
allowNestedTransactions is a hack I came up with to do, well, what its name says:
This uses a separate thread to trick the STM runtime into allowing a nested transaction, but the behavior is exactly that of a single-threaded program; the MVar handles control flow and makes sure that the originating thread blocks until the computation on the forked thread completes. Ideally this won’t be necessary in a future implementation of STM. Of course this is only required if “force x” is used as a pure value inside another STM computation, and none of the reactive code calls force except for “occs”. But pure values should act like pure values, so something was required.
(2) Exactly; here are the requirements for a correct future: If f denotes (t0, a), then:
Another way to look at it is to treat each future as having a universe which has its own current time. Then we have the following rules:
So, in “firstFuture”, this code snippet:
says “synchronize this future with f2 until at least time t1″. If (now f2) is less than t1, then this will retry and the transaction should get re-run when either (a) (now f2) >= t1, or (b) the results of maybeSTM (value f2) changes, that is, f2 resolved to a particular value.
(3) I’m sure I haven’t done this properly yet, but it should be pretty easy to solve with a TVar that holds the value of the future once it gets determined; the caching problem seems to apply more to Events (streams of futures) than futures themselves; a future will only ever return its single correct value or retry. I suppose that in the <*> case that the first future could resolve, and then we will go through and re-calculate it when the second future resolves. I’ll think about that and see if I can come up with a good solution.
Overall, STM doesn’t seem -quite- as good a fit as I originally thought; the idea of a value that becomes known through computation and then stays that way is exactly what a pure value does via laziness. I need to think about this problem some more, but it’s been a lot of fun!
22 April 2008, 5:10 pmRyan Ingram:
OK, now that I’ve played with this for a while I have a few real comments about the paper:
(1) You’ve lost the “edge” constructors for events:
You can implement them for Reactive instead of Behavior, but it’s impossible to do so for Behavior itself. That said, maybe that’s a good thing; they can’t possibly detect the exact edge in any system that lets you define behaviors with fmap or arr. I think it’s possible to implement this less powerful operation:
which filters the “tick” event list to only contain occurrences where the behavior changed from False to True between one tick and the next.
(2) The entire reactive system can be parametrized on the definition of Future; futures need the following operations:
29 April 2008, 10:58 amRyan Ingram:
I knew I forgot something…
(3) merging event streams leaks memory; consider a long-running server application of this type:
Somewhere inside of “server” there would be an event join which allows the server to extract data from a connection; this eventually results in an mappend between a future connection and a future character from a connection. Once a connection is closed, it can’t possibly give any more events, but the event merge listens to it forever.
This can be solved with this additional operation on futures:
Implementing whenNever is tricky, though. The best I’ve done so far is have an explicit representation for futures that are Never; another option is to dynamically allow a future to become “Never” although that could break the axioms of futures. The best solution, I think, is to have whenNever delay the second future to the point at which we can determine the first future is never; if it’s immediately obvious (“pure” never) we can return f directly, otherwise we may delay f’s occurrence. This doesn’t break mergeE at all, but it does complicate the semantics of whenNever.
29 April 2008, 11:12 amconal:
Hi Ryan. Thanks for the additional comments.
Agreed, given that a behavior can be any (computable) function of time and the semantics of the edge operations require capturing every transition and capturing it exactly. There are some variations on the
Behavior
type, such as derivative bounds, that would allow catching each transition, while retaining continuous time.I don’t know where the idea of a “tick” could fit in FRP’s continuous-time semantics. Perhaps you’re thinking of a temporally discrete variation.
I may be missing your point. Are you saying that futures are only used through the Future interface?
Why do you expect event merge (
29 April 2008, 11:37 ammappend
) to keep listening to an event that knowably cannot have any more occurrences? An event is represented as a future, and that future can have an explicitly infinite (MaxBound
) time. (See Section 4.5 and Figure 1.) Sincemappend
on events works viamappend
on futures, and the latter handles infinite time, I think your example would work out easily enough without a space leak, assuming that the event reflects the closed connection with an infinite-time future (analogous to a nil at the end of a list).Ryan Ingram:
For (2), yes. I found it very interesting that given the interface to future could be that minimal (four small typeclasses & two or three additional operations) and all of the “definition”-side of behavior/reactive/event could be specified in terms of that minimal interface.
Of course you need to plumb into the implementation details when you are writing the interpretation functions (occs/sinkE/sinkR), unless you take waitFor and force as primitive as well, which severely constrains the design of Future. But most client code can absolutely ignore that and use the behavior/reactive/event abstraction.
(3) From your definition of mappend on events:
Consider the case where v = never. The rhs of <+> goes away, but we still call “merge” in the lhs, and pass v again. So any storage held by v cannot be reclaimed, and there’s also a time leak as <+> keeps getting called needlessly.
The more merges that happen, the worse the leak gets.
29 April 2008, 2:23 pmconal:
Ryan: I see what you mean, now. I think the following two special cases fix this problem, inserted before the one in the paper (quoted in your most recent comment):
I just added these lines to the implementation. Thanks for pointing out the leak.
29 April 2008, 3:29 pmJosef Svenningsson:
I have a very small question about the race function defined at the end of the paper. Is the lock MVar really necessary? I think not but maybe I’m just missing something. In the run function couldn’t you just write to the v MVar first and then kill the other thread. Then double kill will be prevented because the other thread will block when trying to assing to the MVar and will never reach the killThread before being killed himself.
Well, maybe this issue is moot anyway since you and Ryan Ingram seem to have come up with some nicer way of doing this.
30 April 2008, 7:57 amconal:
Hi Josef,
I started out with that simpler definition. Here’s what I think is a problem with it: Concurrent Haskell has no atomic operation for reading an
30 April 2008, 11:13 amMVar
. ThereadMVar
action instead does atakeMVar
followed by aputMVar
. After the reader’stakeMVar
and before itsputMVar
, the race-loser could unblock and do itsputMVar
. This secondputMVar
to the sameMVar
would break the pure semantics and would cause the reader to block instead of completing itsreadMVar
. I hadn’t considered this possibility until Ivan Tomac pointed it out.Jake McArthur:
I discovered an error on page 8. Your definition of joinR:
Before completing the definition of
u
, you say that it is the first of either(`switcher` Ev ur) <$> ur
orjoin urr
.In actual definition of
7 May 2008, 8:32 amu
in your paper, you usee'
, which was never bound. I believee'
is supposed to beEv urr
. Before completing the definition ofu
, your intermediate code ((`switcher` Ev ur) <$> ur
) uses neithere'
norEv urr
, but usesEv ur
instead.conal:
Thanks, Jake! There were two mistakes. Errata and paper source updated. Here’s the correct code:
Max:
So, when are you going to release the library that goes with this paper? I’m really looking forward to playing around with this stuff!
30 August 2008, 1:37 amConal Elliott » Blog Archive » Why classic FRP does not fit interactive behavior:
[…] The Arrow style required multiple behaviors to be bunched up into one. That combination appears to thwart efficient, change-driven evaluation, which was the problem I tackled in Simply efficient functional reactivity. […]
9 December 2008, 10:45 pmivan:
Could you post a changelog between the most recent version of this paper and the new push-pull functional reactive programming paper, please?
30 June 2009, 7:57 amconal:
Hi Ivan. I don’t have easy access to the detailed differences. There was very little change (lots of revision already). Mainly:
joinE
in 7.2.2unamb
“reference implementation” in Figure 4, noting that it is not an efficient implementation.