The fatal attraction of FRP is its simplicity. Semantics are beautiful and clear. There are Behaviors, functions of time, Events, timed occurrences, and they all dance together. You write causal transformations (future values depend on past values, not vise versa), and it works. You use equational reasoning to transform your program. You get inspired by Conal Elliott's papers.
However, how do we implement it? Many a ship has foundered in these waters.
Perhaps you do not even have to. People do different things to really "get," obtain an operational understanding of something. Mathematicians like denotational semantics. They understand object A by explaining it in terms of object B we already know. Only programmers like to implement things, but then they can use an existing implementation to play with. Once I play with Impl(A), I "understand" A.
At the Budapest functional programming beer night I used to go to, we had the fortune of having Gergely Patai and Csaba Hurska. These guys have built simply amazing Haskell projects, the Elerea FRP library, and the LambdaCube purely functional 3d engine. Gergely is a mathematician. He explained FRP to me by reducing things to Cartesian closed categories. As you guess, it did not help me - to this day my understanding of those things is shaky. Csaba is a little more of a programmer, he told be Haskell is a hacker's language. Both, now that I think of it, made fun of me for talking about dependent types being the future of programming, with good reason, as I now know. But I digress.
At a very high-level, problems with implementing FRP in a language like Haskell or F# are these:
- It is quite hard to enforce causality of transformations with a vanilla type system. AFAIK most practical systems either design a new language and type system, or just leave this invariant unchecked.
- If event streams are first class values, this can easily create memory leaks in a higher-order program, if the entire unfolding history is retained. Solutions here are: do not give the user first-class event streams (but only their transformers, or something like that), create a new language and type system that rules out those nasty higher-order programs (Elm.js), or use some kind of combination of convention, types, and clever implementation (weak pointers etc), to make it work.
Much as I despair getting FRP right, I am coming back to the topic as we are working on WebSharper UI.Next with Simon Fowler. While trying to implement a sub-FRP system, something that is not quite as general but hopefully much simpler to implement, I also realize I start slowly thinking about general FRP again.
Briefly, the compromises we are making in UI.Next are:
- No first-class event streams. You want to transform event occurrences? Do it with imperative state and callbacks. That stops working? Do it with agents and communicating micro-processes. That stops working too? Use Concurrent ML (or Hopac). We are not quite there yet with providing CML primitives in WebSharper/JS but this is planned, probably as a proxy for Hopac.
- We do, however, provide a dataflow abstraction that is almost, but not quite, approaching FRP behaviors. A View<'T> is something that varies over time, in discrete steps. It is now computed from zero or more Var<'T>, which are a reactive form of ref cells.
- Using the mental model of communicating processes, we designed a protocol for View<'T> processes to synchronize in a way that is friendly to GC for most programs, even without relying on weak pointers (not at this point, at least). The protocol also does not preserve occurrences, it synchronizes to the "latest" value, which is nice.
- Views can be observed imperatively or by constructing reactive DOM documents.
It is tempting to add Behaviors too, perhaps as View'T>.
As I am playing with this, I have one rebellious thought. Are FRP event streams or their transformers even worth the trouble? Behaviors are clearly wonderful. But do you really want first-class "mouseClicks"? Or a fold on those? In the context of an ML type system such as F#.
It remains to be seen. So far I like that our system is working well with embedding typical higher-order, stateful widget code, as found left and right in the JavaScript ecosystem. It is also simple enough so we could implement it.. Reasoning laws are NOT the ones for pure FP, but things are mostly tractable if you stick to the paradigm of communicating processes. CML might just fill the spot, but some cross-pollination might also happen. That is, we might end up adding process combinators that look a lot like FRP event and event/behavior combinators.
No comments:
Post a Comment