Introduction
Currently, there is a lot of churn in web frontend technology: libraries and frameworks are created almost weekly, to get obsoleted and forgotten just as quickly. Most of the churn is presented as innovation. It seems to me that programmers are just too eager to jump onto the next "newer, better" technology (well, at least I had to do it a few times at work: just pick something, and hope it will work for your project).
Which is where we come to Ur/Web. What is it?
The Ur/Web programming language is a project by Adam Chlipala, and it gives some firm footing to web programming. With Ur/Web, the decades-old theoretical knowledge (as well as some pretty novel stuff) and practical experience in programming languages is directly applicable to the building of practical, everyday web applications!
Why would somebody want to write an application using Ur/Web?
- the ideas of statically typed, higher-order programming are gaining ground in the industry (see Swift, Rust, Flow, and even TypeScript could be put here, though it isn't "typed" in the same way as the other examples)
- pure, functional, reactive programming practices are gaining popularity among web frontend developers (witness the popularity of React and similar technologies, e.g. Vue.js, Inferno, Riot, etc.)
Ur/Web shares all of these traits: it's typed, higher-order, pure, functional, and reactive. It's been around for quite some time. Now, enough with the talk, right? So let's build some applications instead! I certainly find this more rewarding.
Let's think about the application we will be building. The idea, of course, is to build a (simple) app for myself to use. e.g. a chipin calculator! Since the chief complaint about Ur/Web from practitioners has been that it's highly anti-modular (breaks MVC, lacks any separation of concerns, etc.), I'd like to explore how to structure Ur/Web applications so that they are modular (and hence, can be easily modified).
The application
The app itself is very simple: it allows you to enter people and the amounts they have given to cover some shared cost to have it calculate for you who owes what to whom. The idea is simply that costs should be shared equally among participants.
The application can be in different states: initial, and calculation results. In the initial state, let the user input persons and their amounts (add, remove, edit persons), then press the "calculate" button to have the app transition to the calculated state (where user will be able to see the results). The "calculate" button can be pressed only if all contribs are valid/positive/non-empty names; and there are at least two contribs.
The code
Model and actions
As you can probably see on GitHub, in the code we define some application-specific types first: contrib
, chip_in
, payback
and contrib_list
. In particular, contrib_list
is the model, aka the state of the application. Then we define some actions that will mutate the model: the functions like insert_contrib
and delete_contrib
will simply perform the processing. As an aside, in SAM, the responsibility to make changes to the model is contained within a single "present" method, while "actions" will only create well-formed messages that are pure w.r.t. the model. My guess is that we can always emulate this approach in Ur/Web too, if necessary.
Since Ur/Web is pure, how is mutation possible? Ur/Web defines an abstract type source a
(for some type a
), that works as a mutable ref
-cell. Say, if you have x
of type source int
, then you can set
its new value or get
its current value. Of course, all of these operations only work in the transaction
monad.
Rendering the model
Ur/Web provides the signal
monad, for implementing FRP (functional-reactive programming) principles. To put it briefly, if you want to put something dynamic on the page, all the dynamism will usually come from reactions to changes of some source
. For simplicity, form elements (like input
s) can be hooked to source
s, providing values for them, that is, as soon as user types something into an element, the source it is hooked to will have its value updated.
How does the page react to changes to a source? In the signal
monad, it is possible to "extract" the current value of the source
(operationally: we subscribe to changes to the source), and use it in a further computation, which will usually end up with code producing some markup.
As you can see in the code, we have the function state_representation
that computes the representation of the model's state (which is the HTML view!). For instance, we store the current "page" as a source appPage
, then we subscribe to changes of page, which involves branching and calling different functions depending on the current page. Simple!