From 89ea850245c506b7eb123b80b91b081851fcfe7a Mon Sep 17 00:00:00 2001 From: Steve Krouse Date: Mon, 30 Jul 2018 16:30:32 +0000 Subject: [PATCH] ## FRP Essay, draft 2 started MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * TOC {: toc } ### Relaxing reading on Friday afternoon I didn't feel like working on my computer, so I printed out my essay, Jonathan Edward's feedback, and grabbed a few papers to re-read I felt would be relevant. Turns out they really were! From [What's Functional Programming All About?](http://www.lihaoyi.com/post/WhatsFunctionalProgrammingAllAbout.html) > The core of Functional Programming is thinking about data-flow rather than control-flow. > it's about managing the same complexity in a way that makes the dependencies between each piece of code obvious, by following the graph of where function arguments come from and where return values end up. From [Out of the Tarpit](https://github.com/papers-we-love/papers-we-love/blob/master/design/out-of-the-tar-pit.pdf): > There is in principle nothing to stop functional programs from passing a single extra parameter into and out of every single function in the entire system. If this extra parameter were a collection (compound value) of some kind then it could be used to simulate an arbitrarily large set of mutable variables. In effect this approach recreates a single pool of global variables — hence, even though referential transparency is maintained, ease of reasoning is lost (we still know that each function is dependent only upon its arguments, but one of them has become so large and contains irrelevant values that the benefit of this knowledge as an aid to understanding is almost nothing). Out of the Tarpit turned me on to Dijkstra's [The Humble Programmer](https://www.cs.utexas.edu/~EWD/transcriptions/EWD03xx/EWD340.html): > A study of program structure had revealed that programs —even alternative programs for the same task and with the same mathematical content— can differ tremendously in their intellectual manageability. A number of rules have been discovered, violation of which will either seriously impair or totally destroy the intellectual manageability of the program. These rules are of two kinds. Those of the first kind are easily imposed mechanically, viz. by a suitably chosen programming language. Examples are the exclusion of goto-statements and of procedures with more than one output parameter. > Argument four has to do with the way in which the amount of intellectual effort needed to design a program depends on the program length. It has been suggested that there is some kind of law of nature telling us that **the amount of intellectual effort needed grows with the square of program length**. But, thank goodness, no one has been able to prove this law. And this is because it need not be true. We all know that the only mental tool by means of which a very finite piece of reasoning can cover a myriad cases is called “abstraction”; as a result the effective exploitation of his powers of abstraction must be regarded as one of the most vital activities of a competent programmer. In this connection it might be worth-while to point out that the purpose of abstracting is not to be vague, but to create a new semantic level in which one can be absolutely precise. Of course I have tried to find a fundamental cause that would prevent our abstraction mechanisms from being sufficiently effective. But no matter how hard I tried, I did not find such a cause. As a result I tend to the assumption —up till now not disproved by experience— that by suitable application of our powers of abstraction, **the intellectual effort needed to conceive or to understand a program need not grow more than proportional to program length**. (Bold is mine.) Reading Dijkstra was a revalation. This quote almost makes my part of my first FRP draft look like plagiarism for not attributing this idea to him! > the intellectual effort needed to conceive or to understand a program need not grow more than proportional to program length Honestly, I didn't know! But know I do. ### What my FRP essay is really about I'm realizing that much of my essay need to be deleted, which is sad, but its important to be ruthless about "killing ones darlings" when it's time to. I went ahead and deleted it all and started afresh in the same file, but I expect that I will be coming back to the [first draft](https://cdn.rawgit.com/stevekrouse/futureofcoding.org/01f04f48eab8bd4740beb667ae3fb03b4aaf5b0c/drafts/frp.md) a lot to copy and paste various elements. I see now that my essay is less about the importance of explicit dependencies, but more about how Elm doesn't have them, but Reflex does, and the reason is higher-order and cyclic streams. I worked on this essay for ~90 mintues today and it was a bit pulling teeth. I'm not sure if it's because I'm in a new setting -- the NY Public Library instead of home -- or if I'm sad about the mass deletion, or distracted by my other part time work, or whatever, but I'm going to stop working on this essay today in favor of other part time work, and start again tomorrow morning. --- drafts/frp.md | 249 +++++--------------------------------------------- 1 file changed, 21 insertions(+), 228 deletions(-) diff --git a/drafts/frp.md b/drafts/frp.md index fc1faea..ba37735 100644 --- a/drafts/frp.md +++ b/drafts/frp.md @@ -2,136 +2,24 @@ title: FRP --- -# Modular Comprehensibility and FRP +# Explicit Functional Reactive Programming * TOC {: toc } -### Abstract - -When trying to comprehend a section of an unfamiliar codebase, developers spend an amount of time disproportionate to the size of the section, a concept this paper establishes as "modular comprehensibility". This is particularly relevant for would-be open-source contributors, who have limited time, and often only want to make changes in a small number of sections. This paper demonstrates how higher-order Functional Reactive Programming, such as the Reflex library, achieves a high level of comprehensibility modularity for user interface construction. The use of such a paradigm could massively decrease the on-boarding time for developers to make effective changes. - ## 1. Introduction -Virtually all software development work is modifying existing code. Before you can make a change, you must understand how the code currently works. This often is the bulk of your effort. Making the modification itself is often very simple, and just a few lines of code. It's a worthwhile goal to minimize the time for a developer to comprehend enough to make an effective edit. - -Code comprehensibility is particularly relevant to open-source, because Open-source works best when the user of the software can also edit the source code. What's the point of being free to read the code if you cannot understand it? - -I believe that our programming model is to blame for the current level of difficulty developers face in comprehending a unfamiliar project, and below argue in favor of a model that improves one aspect of code comprehensibility, the modularity of code comprehensibility. - -The contributions of this paper are: - -* Establishing "modular comprehensibility" as a new frame to evaluate programming models. -* Demonstrating how implicit dependencies and side effects decrease modular comprehensibility, while explicit dependencies and pure functions increase it. -* Articulating the modular comprehensibility trade-offs between two Functional Reactive Programming models: the first-order, non-cyclic Elm Architecture with the higher-order, cyclic Reflex. - -## 2 Background - -### 2.1 Modular Comprehensibility - -If you wanted to understand the entirety of a software project, you'd have to read every line of code. But what if you only wanted to understand a small part, such as how a particular button behaves? Can you get away with only reading a similarly small piece of the code? In other words, is module comprehension proportional to the amount of code read? We can refer to such a project as "modularly comprehensible." - -![image](https://user-images.githubusercontent.com/2288939/42785649-4e5f98b6-8921-11e8-9f23-34b44e35b804.png) - -Most programming languages are not modularly comprehensible in such a linear fashion but in an exponential one, as illustrated with the yellow curve. Understanding is very limited until you've read almost all the code, at which point it expands very quickly. - -The lack of modular comprehensibility slows down the time it takes a programmer to make a change to an unfamiliar project. This is particularly relevant in open-source software, because developers have limited time to contribute. It's also particularly relevant with front-end code, which is notoriously difficult to comprehend. Have you ever wanted to make a small bug-fix or improvement to an open-source project, but gave up after a few hours of failing to understand how the code works? - -### 2.2 Data dependencies - -Most code is not modularly comprehensible because the way data dependencies between modules are organized. The follow graph represents the relationships between pieces of data. - -![group 1](https://user-images.githubusercontent.com/2288939/42883646-314fa9b0-8a6a-11e8-9864-d1423eaff6e2.png) - -In a traditional imperative langauge, modules affect other modules *at a distance*, such as `F.update(10)`. The arrow relationship is owned by the tail. Modules specify which other modules they *affect*. - -![group 2](https://user-images.githubusercontent.com/2288939/42883693-502ade40-8a6a-11e8-886d-d3c14a97fac1.png) - -From the perspective of modulary comprehensibility, this is the opposite of what you'd want. Let's say you wish to understand the behavior of `F`. You grep for `F.update` in the codebase. For each `F.update` , you have to go to that call site and understand its context well enough to know what triggers that line of code, and the value of the arguments it will pass in. If you're lucky, all that information is self-contained, but you'll likely be forced to expand your understanding to yet other sections of potentially irrelevant code. - -To achieve modular comprehensibility, modules should be defined in terms of the modules that can affect *themselves*, their *dependencies*. "This property is common in spreadsheet calculations. The definition of the contents of one cell are always defined just in that cell, regardless of changes happening on the other cells it depends on." [1] - -![group 3](https://user-images.githubusercontent.com/2288939/42883692-50195832-8a6a-11e8-99f7-104eff4747f0.png) - -This way you can understand a module by understanding the modules its defined in terms of, recursively. To understand `F`, you only have to read the `F` and it's children, recursively, highlighted below: - -![group 4](https://user-images.githubusercontent.com/2288939/42883690-50073dfa-8a6a-11e8-9d77-b6f64a8d470f.png) - -This allows you to *categorically rule out all the modules you do not have to read* in order to comprehend the relevant module(s). In the above example, that's all the modules that are not highlighted. If a module is not an explicit dependency (or dependency of a dependency...), it's not relevant. In fact, it's explicitly *independent*. - -### 2.3 Pure functions & disallowing side-effects - -If we wish to have modular comprehensibility, we must have explicit data dependencies to clarify which code is relevant to our present purposes. - -In order to have explicit data dependencies, we must disallow mutation *at a distance*. In other words, we must make our langauge free from side-effects, such as `F.update(10)`. - -This leaves us with a language of pure expressions similar to Haskell. Pure functions work beautifully for batch software that accept an input, do some internal computation, and return an output, such as a compiler. However, programming is ultimately about building software that *affects the world*. Ultimately our software is going to have to *do things*: move bits, open files and sockets, send HTTP requests. How can pure functions represent all that? - -The accepted answer to this question is: they can't. Haskell is split into two worlds: the world of pure expressions and the world of the `IO ()` monad. Life isn't worth living without getting and putting characters to the terminal: - -``` -getChar :: IO Char -putChar :: Char -> IO () -``` - -Just kidding. It's 2018. Who's writing terminal apps? Creating modern user interfaces has very little to do with getting and putting characters on the screen on-by-one. - -### 2.4 FRP - -FRP is a way of declaratively describing interactive UIs *with only pure functions* - no monads required. - -You *declaratively describe what the UI should look like as a function of state*, as opposed to imperatively adding and removing characters to the screen one-by-one. (Of course, there's code somewhere that's adding and removing characters from the screen, possibly in the form of a "Virtual DOM", but we don't have to worry about that - it's below FRP's level of abstraction.) - -I have two helpful metaphors for FRP: "zooming out" and "inverting control". We can see them in action by contrasting the FRP style with its imperative counterpart constructing a button that counts its clicks: - -#### Imperative Button View - -The UI is specified only in terms of its initial state. The code that modifies it lives elsewhere. In fact, code from many different places could modify this button. - -```html - -``` - -#### FRP Button View - -The UI is a pure function from the number of clicks to the button's HTML. The only thing that can affect this button is the value of the `count` argument. - -```javascript -function view(count) { - return `` -} -``` - -#### Imperative Button Update - -How do we calculate the counts? Normally, we deal with events one by one, and the code is organized in terms of events. "When an event occurs, run the following code." - -```javascript -var count = 0 -document.getElementById("counter-button").onclick = e => { - count++ - document.getElementById("counter-button").innerText = count; -} -``` - -#### FRP Button Update - -But let's zoom out. Instead of dealing with events one-by-one, consider all the click events that will ever happen on this button as ah infinite list or "stream". Next, let's define our data in terms of this stream, so that the data is now top-level and events are subordinated as dependencies of data. - -```javascript -const count = DOM.select("#counter-button").events('click') - .reduce((accumulator, currentValue) => accumulator + 1, 0) -``` +Explicit dependencies clarify which sections of code are independent to our current investigations, keeping the "effort needed to concieve or to understand a program ... not more than proportional to the program length" [Humble Programmer]. -You may be wondering how `count` can be a `const`, even though it's value needs to update when button is clicked. This is because `count` is itself a stream of values that can be used as a dependency in streams defined elsewhere. In other words, `count` is constant in the sense that there's no code anywhere that can modify `count` besides its definition above. `count` is read-only. +In pure functional programming, all terms explicitly list what they depend upon - as opposed to imperitive programming where terms can "be dependent on many, many different hidden mutable variables" [Out of the Tarpit]. -You also may be wondering about `DOM.select("#counter-button").events('click')` in the FRP Button Update example. Selector-based event querying is how CycleJS works [2] and fits this specific example best. +However, functional programming isn't immune to hidden dependencies: -You may also be wondering about how the `count` stream get passed into the `view()` function? Good questions! Below we will discuss a more semantic approach that exposes the true cyclic nature of this button. +> There is in principle nothing to stop functional programs from passing a single extra parameter into and out of every single function in the entire system. If this extra parameter were a collection (compound value) of some kind then it could be used to simulate an arbitrarily large set of mutable variables. In effect this approach recreates a single pool of global variables — hence, even though referential transparency is maintained, ease of reasoning is lost (we still know that each function is dependent only upon its arguments, but one of them has become so large and contains irrelevant values that the benefit of this knowledge as an aid to understanding is almost nothing). -FRP implementations have been plagued with space and time leaks. In other words, they take up a lot of space and are slow. While many UI libraries are inspired by FRP, they do not adhere strictly to only *pure functions*, in part for performance reasons. For example, many FRP libraries disallow higher-order streams (streams-of-streams), which as we will see below is ultimately bad for modular comprehensibility. +The popular Functional Reactive Elm Architecture suffers from this problem. I argue that the way to maintain explicit dependencies in Functional Reactive Programming is through cyclic and higher-order streams, as demonstrated by the Reflex framework. -## 3. The Elm Architecture +2. The Elm Architecture Elm is a pure functional language in the spirit of Haskell that compiles to JavaScript. It is an FRP-inspired langauge that only allows first-order and non-cyclic streams. @@ -141,16 +29,12 @@ The Elm Architecture follows directly from the first-order, non-cyclic restricti The Elm Architecture was built originally for use in Elm, but has since inspired ReactJS's Redux, VueJS's Vuex, CycleJS's Onionify, among many other front-end state management libraries. -While The Elm Architecture does adhere to the letter of the comprehensible modularity law - only pure functions, no side effects - it does not adhere to the spirit of the law - no mutation from a distance, no implicit dependencies. This design decision was made consciously in Elm to decrease coupling. [3] - Let's examine the architecture. The reducer is a function that takes the old state and an event, and returns a newly computed state. ```haskell reducer :: state -> event -> state ``` -Like in the "FRP Button View" example above, the "view" (HTML and CSS) of the application is defined declaratively in terms of a `state` variable. In the Elm Architecture, the state is a singleton value. - The way the view sends events to the `reducer` differs between the frameworks. As we saw above, CycleJS derives event information *outside* the view, such as `const clicks = DOM.select('#counter-button').events('click')`. React and Elm generate events from *within* view, such as `