The Misunderstood Roots of FRP
futureofcoding.org
futureofcoding.org
Let's face it. FRP kinda sucks. It's awkward as hell to use and takes way to many brain-cells to achieve simple results. Even the experts have trouble with it. I remember e-mailing the author of a popular FRP library once asking him how I could get multiple shapes moving on the screen at once (as opposed to just one). He replied that he had no idea. Apparently it was still an open problem he was working on. Yikes!
State turns out to be a convenient, natural way to represent... state. Funny, isn't it? That if you have some real world problem that involves state, that you want to model that using virtual state and not the high-order time-varying continuous functions that FRP uses. Even Alonso Church admitted that Turing's tape-based model of computation was easier to understand than his lambda calculus.
BTW one more thing. Spreadsheets are commonly cited as a good example of real-world FRP use. The great irony is that nobody on the planet thinks of spreadsheets as time-varying continuous functions. Nope. People view spreadsheets as a big grid of numbers... i.e. state.
So this points out a flaw in thinking i think : that you own the whole system.
I think the reason FP and FRP is hard to use is because a lot of work involves interfacing with existing systems that are inherently created imperatively (the examples in the article - files, network IO, etc).
It is the same story as LISP machines - the idea is great, but the existing infrastructure and computing devices all rely on register based machines, and run imperative instructions means that at some point, somebody had to write code to work with those machines, and that code tends to infect upwards to the high-heavens of clean abstraction, taking it down. And this happens on every level - because one implementation details is another's interface.
I don't think the author is disputing this at all, but positing that for writing application layer programs this is unnecessary complexity and ought to be abstracted by the language/compiler. Perhaps as it stands our programming languages are too general purpose; in future, will we see a clearer distinction between languages for different purposes?
> The great irony is that nobody on the planet thinks of spreadsheets as time-varying continuous functions. Nope. People view spreadsheets as a big grid of numbers... i.e. state.
People view some spreadsheet cells as state, and they view others as functions. There's a distinction between things that are naturally represented as one, vs the other.
This is why I preach to people about MobX whenever I get the chance. It's a Redux alternative that embraces state for things that make the most sense as state, while giving you tools to easily FRP not only React content, but any pure function of that state.
But a cell with a function is not a function, it’s a result of calling a function at a certain set of values. That is even true if you only use ArrayFormula.
E.g. you can’t define a formula inside a cell and the call that cell with certain arguments.
In Redux one explicitly, imperatively sends messages to a top-level message dispatcher, which then invokes more imperative code.
This feels /very/ different to Reflex's folding over events, and transforming dynamic values with pure functions.
The API you use is pretty different as well; consider:
* https://github.com/reflex-frp/reflex/blob/develop/Quickref.m...
* https://github.com/reflex-frp/reflex-dom/blob/develop/Quickr...
I think Redux isn't true FRP because it's very much about defining state. You plainly state every variable your program needs, while in FRP, variables just don't exist.
"It's awkward as hell to use and takes way to many brain-cells to achieve simple results."
> Even I find the above [react.js FRP] code hard to read, despite having written it! While this could be seen as a downside of the denotative approach, I see it as a mismatch between the denotative approach and textual syntax. There’s been interesting work visualizing streams, which I hope to build on. Currently, I’m in the mockup phase, but I hope to build a developer tool experience that will automatically generate a live visualization.
My understanding is that right now it certainly does require too much brain to do anything in FRP, but that isn't a downfall of FRP as a concept ("FRP kinda sucks") but rather that making FRP convenient is still today an open research question.
"That if you have some real world problem that involves state, that you want to model that using virtual state and not the high-order time-varying continuous functions that FRP uses."
> Denotative languages better convey the global structure of a program. We can fully understand an expression by its subexpressions, and their subexpressions, recursively. There are no spooky action-at-a-distance side-effects that can manipulate things from afar. We don’t have to read the entire codebase to ensure we understand a single piece; we must merely read its subexpressions, recursively. This allows us to quickly rule out what we do and do not need to read, saving us a lot of time in large codebases.
I don't disagree that using concrete, non-abstracted state is a simpler way to model state-related problems, but this isn't about modeling and solving state-related problems, it's about solving them in a way that's maintainable. It's easy to argue for simplicity now (i.e. when first approaching a problem) and we have many, many tools for using state for that, like ipython notebooks, or pen and paper. But let's point out that simplicity of these initial concrete-state-minded approaches tend to grow into entangled messes, messes that are arguably solved by encapsulation and patterns. Regardless of whether they do solve the problem, having to understand things like inheritance and all their related problems kind of defeats the purpose of using state to make the problem simpler to understand in the long run. Which is what FRP purports to solve. Except we don't have the pen and paper for FRP yet.
"The great irony is that nobody on the planet thinks of spreadsheets as time-varying continuous functions."
True. No one thinks that. The author doesn't think that. Probably not even the folks who made the neat stats-oriented spreadsheet[0] thinks that, and that's data as continuously varying as you can get.
All of this kind of props up the title which is that FRP is grossly misunderstood.
celsiusTextbox.whenChanged do
fahrenheitTextbox.value = cToF(celsiusTextbox.value)
end
What you wouldn't say is something like: "The Fahrenheit textbox's value depends on whether the Fahrenheit textbox or the Celsius textbox was edited more recently. If the Fahrenheit textbox was edited more recently, its value is equal to the value the user entered into it most recently. If the Celsius textbox was edited more recently, the Fahrenheit textbox's value is equal to the converted value of the value the user entered into the Celsius textbox most recently." But that's the program you have to write in the "DCTP" approach proposed by the author.I think this is true of a lot of UI programming: most people intuitively think about it in terms of "when X happens, do Y", so trying to cram it into another programming model is more trouble than it's worth.
I think your example is exactly why stuff like React won vs data-binding frameworks. With declarative programming, you just have a single source of truth that you change once (you can arbitrarily pick celsius or fahrenheit or even kelvin) and let the framework figure out what needs to be diff-ed in the derived views.
(The examples here are for buttons and sliders, but it's the same syntax for input fields)
They weren’t beautiful, but both methods worked fine for the rare cases they proved necessary.
I think an example of database programming in "Continuous Time" would be
(datomic.api/q '[:in $ :find ?e :where [?e :post/title]] db)
where `db` is a time-pinned and consistent value of the database graph – in other words datomic.api/q is a conceptually pure function of time, time is a parameter, which implies you can rewind it or speculate into the future (both of which are supported by this interface)As for abstracting HTTP – what if you considered network io as just a way to lazy load a cache of immutable database values? So for example, this would work a bit like Git – we don't care how the clone protocol works, just load me the file values I identified. Maybe it uses HTTP, maybe it uses something different, who cares? Get me the value I asked for in the fastest way possible given available infrastructure, distributed caches, etc.
Then, the question of IO resolves to: what categories of effects can be modeled as values and functions on values? Given declarative data programming in Clojure – basically anything!
If you model this in a declarative way then you have to be careful to avoid implying that all events should be remembered, and also avoid depending on anything in the future unless you want to wait for it. This makes designing an intuitive FRP-based language pretty hard.
Is that true in a post-AWS world with storage getting cheaper faster than you can consume it? Maybe don't put 4k video in the log. But, you're right, we can stream, shard and forget as necessary.
> also avoid depending on anything in the future unless you want to wait for it
Can you elaborate on this, my gut reaction is to ask why I need to avoid depending on git commits that haven't been written yet – it's kind of a weird question right? Time is explicit now, which means you have the right knobs you need to coordinate it, even across distributed nodes.
This might be true of "storage" as in disk space, but it definitely isn't true of "storage" as in RAM. If your phone kept an in-memory log of every single click event, you'd run out of RAM pretty fast.
The conceptual idea of a value that continuously changes is nice for animation or maybe video (though that's both discrete and lossy), but seems messier when your knowledge is limited to an unknown selection of discrete data points from that timeline, arriving with an unknown amount of lag? Maybe you could talk about that mathematically, but it seems like it's going to be rather abstract and messy.
its similar to tail-call in that the efficacy of your program depends on the compiler figuring out what you're trying to do - which is unsatisfying
but its a great idea and a ray of hope here
edit: this also implies that the set of reads against the history is fully known at compile time - which may make it irrelevant depending on the usage
Yet, I fail to see any other representation where it is viable to solve those problems in a generic way.
If we want to get something better than our current "it's impossible, better not even try" posture, we better look for it somewhere where it is possible.
I like FP because it allows me to be lazy; quoting the author:
> A denotative language resembles a dictionary or encyclopedia, where one can understand an entry by reading it and what it references. A non-denotative language resembles prose, like a novel, which you have to read cover-to-cover to know what happens, even if you only care about one specific character.
Examples that use well-known mathematical functions give a misleading impression that such expressions will be easy to understand. Instead, it can become like puzzling over the meaning of the equations in a mathmatics paper. As the author discovered, this isn't always easy, particularly for unfamiliar mathematical objects, and it tends to appeal more to people with a background in mathematics.
Also notice that the focus on the value of an expression hides all performance issues. Maybe we can specify what an animation should do, but that doesn't mean it will run smoothly. It can be valuable to cleanly separate so-called "correctness" from performance (as if a program that's too slow is somehow correct?), but this doesn't relieve the programmer of the responsibility to work on performance. Languages that don't give you the tools to control performance are incomplete.
- the example is trivial
- the code example is extremely complicated for such a trivial example
- the code has to be explained in minute detail, with at least one visualization (better, two)
- and it still remains largely over complicated
I shudder to think of any non-trivial example with this approach. It will hardly be more comprehensible than existing RxJS or Redux code.
I've spent a bunch of time working in Elm, though I hadn't used it in anger before they dropped the FRP stuff.
My experience with post-frp Elm is that:
* It seems really elegant on small examples * When you start working with larger codebases, and you have some resuable UI elements you want to build, you end up writing a lot of "routing" code to shunt messages to sub-components. Conventional wisdom in the community is to try to keep app structure as "flat" as you can to avoid this, but I've not seen a codebase of meaningful size where this doesn't happen enough to be annoying.
I have a gut instinct that "real FRP" might shine a bit more at this point; it seems like it would make wiring together different bits of the UI easier.
> Even I find the above code hard to read, despite having written it! While this could be seen as a downside of the denotative approach, I see it as a mismatch between the denotative approach and textual syntax.
Clearly, from this piece and his site as a whole, he’s looking at the long-term picture. Maybe there’s no way to make this better, but we’ve been stuck with the same basic models for decades. I’m glad people are looking for more revolutionary ways to make programming more powerful.
"A functional program is regarded as a mathematical function, describing a relation between input and output."
I think this article is helpful in explaining how insightful that definition is. It's interesting to contrast that with his other classifications of major paradigms at that time. Particularly that of an Object Oriented program (he is one of the fathers of OO with Dahl), which is at odds with the modern viewpoint today ...1. ProceduralProgramming. A program execution is regarded as a (partially ordered) sequence of procedure calls manipulating variables.
2. ConstraintProgramming. A program is regarded as a set of equations describing relations between input and output.
3. ObjectOrientedProgramming. A program execution is regarded as a physical model, simulating the behavior of either a real or imaginary part of the world.
Something I never thought about until I started writing scripts in Excel is that when you define new functions to be used in cell formulas, they are not allowed to have side effects...and it didn't take long before I wanted to write functions with side effects.
Sure, this makes sense from a certain perspective, but when you're using a dialect of Basic, you kind of think it's down and dirty, anything goes. I mean, this is the language that used to usually have "peek" and "poke".
A(B(C(D(E(x)))))
That is why they use the assignments. e= E(x)
d= D(e)
c= C(d)
b= B(c)
a= A(b)
But many people can understand: x-> E -> F-> C-> B-> A
// where -> is a pipe operator.
Now you see that you first execute E with x as input.This shows that Functional program has some kind of problem, if it is not presented in a good way.
There are also other problems. The overuse of Currying is bad, as it hides what is going on. Then we can also have recursion mixed with lazy execution.
I think that the problem with functional programming is the bad presentation of what is really going on.
I try to overcome these problems by using a pure graphical system instead. Everything should be as simple and clear as possible. The system is still in design/development, but you can see some at http://www.reddit.com/r/unseen_programming/
The general idea is that cells like in a spreadsheet are a basis for our functions. Unlike a spreadsheet-cell they can contain multiple variables. These cells can then be combined with pipes and streams. That may already define an functional language, but for me that is just the start.
I'd take the second over the other ones any time of the day, which clearly shows that there is no actual problem to solve here : we all see the world differently and there's nothing anyone can do about that.
The parameters will mess up as soon you have multiple parameters. Especially messy if you want to reuse them. Or when you have conditions.
In a graphical system you do not need brackets, which is why I added it as a possible solution.
At least in my case, it's not an inside-out or left-right thing, it's a funnel that shows the exact relationship of the data. The problem with the second one is that the multiple distinct statements require scanning up and down across them to ensure the local variables aren't used anywhere else, and then mentally reconstructing the data's flow from one function to the next.
It's also why I strongly prefer statements like this:
A(
B(C),
D,
E(F(G))
)
The flow of data is encoded directly in the visual structure, instead of being split out across various statements using local variables that leak intermediate state and introduce accidental mental overhead.For example:
x = A(B)
y = D(x)
z = C(x, E(y))
Is either an important optimization (possibly affecting asymptotic complexity in more complex cases), important for correctness (if any of the functions have side-effects), or at least more succint than the alternatives (in a lazy language like Haskell).i.e. "exhibition of rejects"
https://2019.programming-conference.org/track/sdr-2019-paper...
I believe that this relegates DCTP to the status of a domain-specific language, not a general programming approach: if we know from the start that there are certain computations that we can't/don't want to perform with DCTP, then we know from the start that it only applies to certain domains of programming.
This isn't a problem in itself of course - DSLs can be wonderful things. But this can become a problem if you haven't taken the time to define the domain where your DSL is useful, or if that domain turns out to be extremely narrow. Spreadsheets are a wonderful abstraction for some things, but you wouldn't build an OS with them, nor even a simple web app.
Perhaps they can extend the abstraction until it can become 'nearly general-purpose' (after all, you wouldn't really write an OS in Java either), but I don't think that's guaranteed.
imho FRP is absolutely elegant and wonderful when it all comes together. The difficulty, I found, is iterating and hacking away in order to discover what the end result was meant to be all along. In that respect FRP does slow down development time (at least in my hands. Maybe those with more expertise could iterate faster).
I'd imagine that even in that scenario it could be fruitful to use something less robust for prototyping, and then implement it with FRP for a maintainable release version that's easier to reason about.
If it's the latter, then I'd argue that's part of the exploration of the problem you're trying to solve, no?
In either case, I found it's faster to play around with changes in other approaches, though another way of phrasing that could be that it's easier to break things too :)
can we agree to stop using these meaningless acronyms?!
That said, it's best not to assume.
First, a couple of old talks on the topic -
- "Functional thinking" - Brings together some of the thinking I've applied in a few areas - http://sriku.org/blog/2015/08/11/talk-functional-thinking-fo... .
- "Beta abstraction for bottom-up theory building" - Shows how beta abstraction can be applied to systematically become denotational - http://sriku.org/blog/2016/02/06/beta-abstraction-for-bottom...
Systems -
- In "muvee Reveal", an automatic video production system, the styles are written in a Scheme-dialect called "muSE"[1] in which stuff needed to represent stylistic elements are built. The editing styles DSL [2] and documentation show how this stuff can be represented well removed from implementation details. (Disclaimer: I used to work for muvee, but no longer do so stuff may have changed.)
In particular, video and animations were not modeled as functions of time to Image, but functions of a time interval [t,t+dt] to Image. The reasoning was that the interval information is critical to render motion blur. You could argue that the "dt" is a detail that can be passed on later at render time, but it didn't take away much from the API's simplicity.
- Steller[3] - a library for declaratively composing dynamic temporal structures, useful (and used) for music and synchronized animations.
- elm-anima[4] - a concept demo of structuring animations in Elm. This was pre-18 and so doesn't use subscriptions. Here, animations are conceived of as processes rather than functions over time. I've described the thinking in a post [5]. Elm fell out of favour for me as there were many APIs I needed to work with that I couldn't use it with and the portion that needed Elm was small in the systems I was working with.
[1]: https://github.com/srikumarks/muSE
[2]: https://srikumarks.github.io/muvee-style-authoring/
[3]: https://github.com/srikumarks/steller
[4]: https://github.com/srikumarks/elm-anima
[5]: http://sriku.org/blog/2015/12/13/towards-reactive-animation-...
Also, with your Elm-anima project, what were the lessons and takeaways? It seemed like it was working, but what are the limitations of the approach?
Elm-anima - yes I felt that the elm-anima approach worked fine conceptually and is fairly performant too with scope to inject caching and laziness, but its approach ran into 0.18 's subscription mechanism (it was done before subscriptions) and I couldn't wrap my head around the loss of control there for this purpose. Things didn't compose after that.
Edit: elm-anima also needs more work to support effects, without burdening the API too much, to be truly useful.
Personally, I'd have liked the whole program in Elm to be modeled as just an "Automaton Input (Html, Task x ())" or something. That would've given enough freedom to do these kinds of things and, with some effort, be performant too.