Fancy reactive web solutions on counter example
Some people are fascinated about spreadsheets. This lovely type of software was offered in Apple II computer back in 1979. Every cell could contain either data (text, numbers) or formula. What’s so special about it? Formulas are reactive. And that introduces the idea of reactivity.
Let’s have a look at few examples in some experimental technologies touching this idea.
What we’re looking at?
All programs below are compiling to / running on top of HTML + JavaScript and consist of:
- two buttons to increment and decrement counter value
- a paragraph that displays the current counter value
- a paragraph that displays the previous counter value
- a paragraph that displays some other value that will not change in future
which looks like this:
Vanilla JS
To show the amount of effort needed in other technologies let’s start with pure JavaScript:
(() => {
let $id = (id) => document.getElementById(id)
let container = $id('main-container')
let btnDec = container.querySelector('.decrement')
let btnInc = container.querySelector('.increment')
let pVal = $id('counter-value')
let pValOld = $id('prev-counter-value')
let pOtherVal = $id('other-value')
let state = {
counter: 0,
previousCounter: 0,
otherValue: 100
}
function updateView() {
pVal.innerHTML = 'Counter: ' + state.counter
pValOld.innerHTML = 'Previously: ' + state.previousCounter
pOtherVal.innerHTML = String(state.otherValue)
}
btnDec.addEventListener('click', () => {
state.previousCounter = state.counter
state.counter -= 1
updateView()
})
btnInc.addEventListener('click', () => {
state.previousCounter = state.counter
state.counter += 1
updateView()
})
// show initial value
updateView()
})()
and important part of HTML for it:
<body>
<div id="main-container">
<button class="decrement">Decrement</button>
<button class="increment">Increment</button>
<p id="counter-value"></p>
<p id="prev-counter-value"></p>
<p id="other-value"></p>
</div>
</body>
Nothing special. We need to remember about calling updateView() function every time when state is changed.
Cycle.js
This framework is built on top of Observables which refer to Functional Reactive Programming. Observable is a stream of values which come into stream through the time. The idea doesn’t make data and “formulas” (bindings, projections) timeless like spreadsheets do.
Due to libraries, among alternatives like rxjs and most I choosed the xstream here:
import xs from 'xstream'
import {run} from '@cycle/xstream-run'
import {div, button, p, makeDOMDriver} from '@cycle/dom'
function main(sources) {
const initialValue = 0
const otherValue = 100
let action$ = xs.merge(
sources.DOM.select('.decrement').events('click').map(ev => -1),
sources.DOM.select('.increment').events('click').map(ev => +1)
)
let count$ = action$.fold((acc, change) => acc + change, initialValue)
let prevCount$ = xs.combine(count$, action$).map(([acc, change]) => acc - change).startWith(initialValue)
let otherValue$ = xs.of(otherValue)
let state$ = xs.combine(count$, prevCount$, otherValue$)
return {
DOM: state$.map(([count, prevCount, otherValue]) =>
div([
button('.decrement', 'Decrement'),
button('.increment', 'Increment'),
p('Counter: ' + count),
p('Previously: ' + prevCount),
p(String(otherValue))
])
)
}
}
run(main, {
DOM: makeDOMDriver('#app')
})
and important part of HTML for it:
<body>
<div id="app"></div>
</body>
Map all click events to actions (action$ variable) which are summarized into count$ and in the end the state is projected into view (DOM). The produced DOM is of course observed and mapped to actions$.
To summarize, program goes from starting values through events (actions) to combining a state projected into view (DOM) which is updated by state and watched for more events to come. You may have got lost but that’s a cycle. DOM comes as input and DOM is a result of state projection.
The great part about Cycle.js is a really frictionless way to put in animations for data changes, at least comparing to React which is so popular. The drawback here is that this task sure needs some code BUT since it’s all about functions - you can write some parts once and reuse it.
Hoplon on Clojure
Hoplon is a library for Clojure language.
(page "index.html")
(defc counter 0)
(defc prevCounter 0)
(defc otherValue 100)
(html
(body
(button
:click #(
(reset! prevCounter @counter)
(swap! counter dec))
:text "Decrement")
(button
:click #(
(reset! prevCounter @counter)
(swap! counter inc))
:text "Increment")
(p :text (cell= (str "Counter: " counter)))
(p :text (cell= (str "Previously: " prevCounter)))
(p :text otherValue)))
Most of the code is a HTML-like template filled with click event implementations. The interesting part of Hoplon are cells. Like spreadsheets cells. Note that defc counter 0 should be read as “define cell named counter with initial value 0”. Like in spreadsheets, cells could contain values or formulas.
(defc counter 0) is equivalent of (def counter (cell 0)).
There is also a syntax for formula. For example, if we wanted a sum of counter and otherValue then formula for cell named counterPlusOtherValue would be defined this way:
(def counterPlusOtherValue (cell= (+ counter otherValue)))
In the snippet above there are some inline formula cell injected into paragraph text:
(p :text (cell= (str "Counter: " counter)))
Paragraph text will get updated every time the counter cell is changed. It’s like really like formulas in spreadsheets.
To learn more about Hoplon I recommend this talk:
Eve
Now that’s something oddly different. Eve is about writing a document. Such document consists of text, images, charts and code blocks of course.
Below are my custom text headers View, Model and Actions with respective code blocks:
Above was written with online Eve editor and that’s why it’s screenshot but overall coding in Eve is about writing a Markdown where blocks contain working code and the rest of text is a comment.
commit parts are easy - it’s rather imperative code, usually changing values.
bind may seem obvious but it’s really not. Look closely at this fragment:
[#button text: "Increment", diff: 1]
what is diff doing here? We’re giving a logic value to a visual element. search section does pattern matching against this:
search @event @browser
[#click #direct-target element: [#button diff]]
commit
// TODO commit some changes...
When engine matches an element which is a button and was just clicked and contains a diff cell then and only then the following commit part will be executed.
So we can see that search and bind sections work as pattern matchers. This example doesn’t really show that clearly but those sections can work in a manner of backtracking, exactly like logic programming languages like Prolog or Datalog.
Eve is more than that and actually more than you would expect, especially when there are logic bugs in your code. I won’t even try explaining it here, I recommend seeing this video:
Elm
Among the previous solutions, Elm is just a functional language that compiles to JavaScript. If you know React + Redux then think of Elm this way but in terms of language instead libraries.
import Html exposing (div, button, text, p)
import Html.App exposing (beginnerProgram)
import Html.Events exposing (onClick)
initialModel = { counter = 0, prevCounter = 0, otherValue = 100 }
main =
beginnerProgram { model = initialModel, view = view, update = update }
view model =
div []
[ button [ onClick Decrement ] [ text "Decrement" ]
, button [ onClick Increment ] [ text "Increment" ]
, p [] [ text ("Counter: " ++ toString model.counter) ]
, p [] [ text ("Previously: " ++ toString model.prevCounter) ]
, p [] [ text (toString model.otherValue) ]
]
type Msg = Increment | Decrement
update msg model =
case msg of
Increment ->
{ model |
prevCounter = model.counter,
counter = model.counter + 1 }
Decrement ->
{ model |
prevCounter = model.counter,
counter = model.counter - 1 }
What’s so special about it? Strong typing + type inference with option to explicit typing and very helpful errors from compiler (it even suggests fixing potential typos!). For the web. JavaScript is totally not like this.
view function is actually pretty same as in previous languages. It defines view, what data projects into it and what events are translated to messages.
For a moment, take a close look at onClick . It’s interesting because it’s imported from some built-in Html.Events package but what input it gets? Our own type: type Msg = Increment | Decrement. It adapts to given type and whenever we try to pass anything else than our Msg to the onClick function - compiler will shout. As you would expect from functional languages.
So it’s overall nice choice between web technologies since instead of choosing language (ES6 / TS / Flow) plus framework (React, Angulars, Vue.js, Ember.js, *) you simply go with a language only. Which is well-thought and that comes out with consistency between rendering and updating state which goes beyond React with JSX in my opinion.
Small comparison of fancy solutions with pure JS
Those examples are all about web. HTML and JavaScript. Web browsers came to be a solution for huge and important business projects so development should be as fluid as possible. ES6 doesn’t give that as much as wanted. Even TypeScript is not that big thing itself - at least to my experience, it’s a minimum if you don’t want to cover compilation errors with “unit tests” (is that really a unit tests when you test passing arguments to functions?).
Sometimes data structure has a mistyped shape and is passed around - what then? In this regard, JavaScript is a shotgun comparing to pistol when you need to shoot yourself in foot accidentaly. Because you’ll know in runtime, not before. Types are good. Of course types can be abused and you may see shooting yourself from another shotgun, just look at templates in C++. However, I believe that Elm is very well placed inbetween those extreme cases of having or not having to type data.
But let’s leave all those type +compilation + data structure concerns. Let’s go back to - why using those fancy solutions? When logic in software is invalid, nothing will help - you may say. Well, at micro level - yes.
Debugging tools can be better than this, like level visualiser of data flow for Cycle.js:
which could potentially grow into a software used for macro level debugging, thanks to collapsing some groups of streams.
- screenshot comes from talk about data flow from André Staltz (timing 6:28):
Another observation is that we don’t need to manually update view with new values. The other fact is about making less updates and updating only the needed parts. So it’s easier to not forget crucial part (updating view) and still having good performance. In the end it’s even less code (to perform view updates) when application grows - even in JavaScript (with Observables and Cycle.js).
But what about application state? Best way to not have errors in state is being unable to produce bad state. Types won’t guarantee that and neither programmer will. Eve and Hoplon shine here. Those two technologies are based on spreadsheets which is actually a concept of having all your data global, rather flat, because it’s easy to look into when debugging the state. Of course, when you say “global” it’s similiar to stores in Redux. But flat Excel-like structure? Yes, that makes things simplier! - But I need my tree-structured data! - Don’t worry, pattern matching from Eve or scoped cells from Hoplon don’t constrain you on that field.
Mutation. It’s so hated these days. Immutability is the king right now. Even in JavaScript - Angular 2 and React promote immutability so badly. Vue.js (+ Vuex) is an exception to this because it takes advantage of mutations for reactivity sake. And that shows us what is really to be hated here is lack of determinism of state changes, view updates and action/intent flow. So, actually it’s not the immutability that fixes things here but this diagram but the flow with one direction.
The last part to discuss is Actions. Every technology solves this in a subtle different way but Eve is extraordinary here. Click event in Eve is not an action to dispatch, it’s rather a situation to be pattern-matched and to be reacted to. It’s very different solution. Personally, in terms of making a real software, I have hard time to wrap my head around this, as much as I had going from imperative to functional or from functional to FRP (like Cycle.js). And indeed, Eve creators don’t recommend going into production with this right now since they are still exploring. But it’s something.
What should I choose?
Elm is a thing and Cycle.js is indeed too. However, between those two I would choose Elm because of handling lists in Cycle.js which is described in issue #312 on Cycle.js GH project. If Cycle.js, then probably on top of TypeScript, which is also recommended by the author of Cycle.js.
About Hoplon, it seems to work and be more similiar to Elm than Eve, even having it’s cells idea. However, years of existence in industry show that not many people like LISP-y style of coding. Probably closing parenthesis is cumbersome. Also, I don’t know how’s about interoperation with JavaScript libraries.
That’s just a reminder but Eve is not production ready, it’s highly experimental. But they say it’s useful anyway.
Is this it?
If you’re so excited to open to more things then look at REBOL - this is a thing founded in 1998 - 18 years ago, now. It’s website and demos are ugly these days. Software examples are so 2000’s but it may be actually not that bad when looked into concepts and problems it solves. For instance, it’s promoted to be well suited for data and meta data visualization which by the way is actually Eve’s big feature, too.
REBOL was made to create window applications. To get a feel about the syntax you could take a look at Minesweeper example which looks like this:
Now just think of that - how hard it would be to implement Minesweeper in Eve, Elm, Cycle.js or Hoplon compared to pure JavaScript? Logic or rendering is not that hard for this example but development is another thing. Development is not only about good code. It consists of producing software, making mistakes, debugging, fixing and testing. Our target should be every tool that reduces mistakes which in result would reduce losing time in debugging.
References
- Talk: Web Programming with Hoplon (Alan Dipert & Micha Niskin) - talk from 2014
- Talk: Hoplon and Javelin, WebDev Alternate Reality (Micha Niskin) - talk from 2015
- Talk: See the data flowing through your app (André Staltz) - about Cycle.js and debugging software
- Talk: Confident Frontend with Elm (Jack Franklin) - comparing Elm to React and telling why Elm
- Video: Eve: a day in the life - short video explaining tooling around Eve lang
- xstream - javascript stream library built for Cycle.js
- counter example made with Cycle.js
- Issue in Cycle.js: Handling lists
- Observables in RxJava
- REBOL: Demos
- Minesweeper example in REBOL
- VisiCalc - probably first spreadsheet software