#21726

I somehow ended up reading An Opinionated Survey of Functional Web Development (2017) by yorhel yesterday. I just went on spending a good chunk of the day exploring the landscape since this article is 9 year old now and preceded a lot of the development in this space since. This turned out to be pretty interesting but I felt like it would be a better fit for a forum post as opposed to an article.

Recap

Let's look at Yoran's bullet points when it comes to the code you need to write for a website.

  • The back end (server-side code) that describes how the input field interacts with the database.
  • Some JavaScript code to describe how the user can interact with the input field.
  • Some CSS to describe what the input field looks like.
  • And then there’s HTML to act as a glue between the above.

Well, indeed for a very long time it seems that we needed these 4 languages just to make a functional website (and potentially one extra for talking to the database, SQL or otherwise -- though not all databases require SQL). We expect enough client-side interaction that we no longer JavaScript in the browser as optional anymore.

Not the problem

SQL and databases

The abstraction to talk to the database is not as much of interest for us. This hasn't changed much since 2017. ORMs, hosted DSLs are both viable strategies. Databases that doesn't use structured queries (NoSQL) have also been around for a long time. All have been tested ad infinitum. There are resident NoSQL experts on this forum so I won't spend more time discussing this.

CSS

This is where we saw a major shift since the yorhel's survey in 2017. Tailwind and the like have completely taken over, largely eliminating the most if not all of CSS from projects that subscribe to it. Maybe we can consider this part solved entirely, for those who consider it a problem. If CSS never felt like an issue to you, especially with the various extensions it has had like variables and nesting, then perhaps this discussion was not relevant for you to begin with.

HTML

I feel that HTML itself is also not really relevant to the discussion. It's in the name, it's a markup language. Many prefer just writing HTML. Almost every serious language has a way to do templates in-language if necessary, either as hosted DSLs, macros, as functions. Some embed HTML directly in the host language. Here's Clojure's Hiccup, for instance:

But the sentiment around HTML itself isn't really all that negative. So of course, the other way around is to embed your language into HTML instead. We see this idea in the classic approach of PHP/ASP/JSP, for instance. Other than those designed with this idea in mind, the rest of the bunch emerge usually when one tries to create a template engine and embed calls to the host language. We see this in most template languages postmortem like HEEx, etc. But there are also funky compiler-based approaches like E4X and XHP, predecessors to JSX, which take us to the main point:

The problem: Reactive rendering (HTML + JS)

This is the biggest shift has happened since then. Not in the sense that it didn't exist 9 years ago. Knockout did it first back in 2010, but in the sense that this area has taken over the mainstream, as well as having seen the most research activity. React of course exploded in popularity. Angular has become irrelevant. And smaller players like Vue and Svelte have taken up some foothold as well.

Even back in yorhel's survey in 2017, bulk of the article was dedicated to exploring alternatives to make this work without writing JavaScript proper. So broadly speaking, the theme of "integrating frontend and backend in the same language" is what the rest of this survey would really be looking at, achieved via different approaches as we shall see.

Naive compiled bundles

Okay, so the obvious answer is to just write all the same frontend reactive stuff in a different language, and have it compile to run in the browser for you under the hood, right? This is already explored in the old survey with options like Elm, PureScript. Although Elm does not let you write the code for the backend, there is Lamdera which lets you do this. PureScript has always been available on both the server (via Node and co.) and the browser. GHCJS (Haskell) has been incorporated into the official compiler but the story is largely the same there. Another somewhat successful option that spawned after the last survey is ReScript, which originated from OCaml. Jane Street has made Bonsai available, which slots into the same category but uses OCaml proper.

But since then there has been more development on this side as well. WebAssembly support across the board has gotten better, but still to this day no WebAssembly tooling story can beat that of Rust. This led to the spawning of a dozen Rust frameworks all around the same idea -- React-esque frameworks but with Rust compiled to WebAssembly: Leptos and Dioxus, among many, many others. There are options in other languages that use the same approach, but it's mostly a Rust thing. Examples lifted from official documentation:

Server-client boundary

All these things are really doing is just replacing React/Vue/Angular/Svelte with something that is not JavaScript-based (and perhaps with a totally different design, like with Elm) but ultimately compiles to do roughly the same thing. So, okay, but can we do better?

Plumbing

In the past 3-4 years we've seen a performance-inspired shift from virtual DOM diffing to signals-based fine-grained reactivity. I know many on this forum really don't care about frontend junk, but signals were popularized by Solid followed by a compiler approach from Svelte. You can read about them here.

If we consider data flow and rendering dependencies as a graph. When upstream changes change, we just need to propagate the changes downstream, and we can stop whenever we have no more downstream nodes to update, or a triggered re-computation yields the value in a node as before. But all of this happen inside the browser. What about changes on the server? Can the presentation in browsers react to those? What about changes on the client? Can we inform the server automatically and get a response to update our view?

Yes, it's possible! And it's not an entirely new idea. Web programming is fundamentally a distributed system problem: the browser and the web server are different nodes, and we must make them communicate and act accordingly. The keyword here is multi-tier programming. The idea is that you would just write how you would write, say, classical PHP, and that the code can be split into two parts -- server and client, where the client side has reactivity over all data, as if you were using a frontend framework, say. You might even be able to do better since these frameworks usually cannot directly react to server-sided changes.

Three such options appeared in 2006, Google Web Toolkit (GWT), Links, and Hop. Yorhel mentioned Ur/Web and Haste, which are in this category as well. Hop is Scheme-based, and it is interesting as the project is still somewhat active. A JavaScript version was released in 2016. Here's a complete chatroom example in Hop from [1].

Another project that received some attention a decade ago is Opa. Aside from GWT these are mostly research or dormant projects. There are some more research prototypes linked on the Wikipedia page for multi-tier programming. As an aside, there are things like Unison and ScalaLoci that are languages specifically designed as for distributed applications, which can in theory be used for this purpose. Koka's effect system also allow this facility by attaching different client and server handlers.

We can further categorize this regime with another property: whether the multi-tier platform can automatically slice code into server and client portions. GWT, Haste, Opa are able to do this, Hop requires manual annotations for which execution context to compile for. There is research going into tier-splitting general purpose languages (that don't have very good metaprogramming). Here's an 2014 research prototype for JavaScript. The corresponding paper [1] also highlights some potential challenges that these system may run into like difficulty in debugging post-transformation or supporting offline-only use (although I don't believe they are fatal to the approach).

Throwback: ColdFusion

I don't know if you remember (I was too young to know anything back then), but ColdFusion apparently used to have this kind of automatic plumbing. You were able to wire Flash clients to transparently call ColdFusion methods on the server. When you write <cfform format="flash">, the form gets compiled to a client SWF that talks to the server directly (no JS/HTML or even ActionScript, all in CFML). The form would be interactive and re-query as necessary as you change the inputs in the cfform. This feels quite ahead of its time, I have to say. I wonder whether ColdFusion would have been able to evolve into a more modern design had Adobe not neglected it after purchasing Macromedia. ColdFusion is still around today just because of the amount of legacy software still paying for support.

Ocsigen

Ocsigen is a fat collection of web-oriented tools for OCaml including the OCaml-to-JavaScript JSOO transpiler which has seen a fair amount of use (...mostly by Jane Street). The Eliom framework provides the sort of plumbing described (without automatic slicing, so manual annotations are required). Yorhel mentioned larger-than-preferred bundle sizes but things seemed to have improved somewhat.

Electric Clojure

The most recent splash in this area is Electric Clojure, which has automatic splitting. The creator gave a quick overview at HYTRADBOI (Have you tried rubbing a database on it?) 2022 that looks very enticing:

The licensing model is a little non-orthodox as the creator's business is anchored on this technology. So this is not quite applicable:

I hope it doesn't turn into another ColdFusion, because it looks very nice and I'd like to try it at some point.

Put EVERYTHING on the server

The server must always be around because there are things that the client can never touch. So we can't eliminate the server, but what if we just try to minimize the amount of code in the browser? Let's ask the server to do everything and just send the updates on the wire. All that the browser needs to do is to receive the updates and shove them into the DOM. So effectively we eliminate any JavaScript we would still need to write. The advantage of course is that it is fast with minimal client JavaScript.

The first project that really took this idea seriously is LiveView from Elixir's Phoenix framework. Roughly speaking, the server will serve the page as normal, but the browser immediately establishes a WebSocket connection immediately with the server. All following view updates happen on this live WebSocket channel with bidirectional communication between client/server. Rendering can react to data changes both ways. Other projects in this category:

  • Gleam also being a BEAM language like Elixir, has Lustre, which can be run in LiveView-like mode as well as compiled to a JavaScript bundle like in the previous section.
  • Blazor started as an option in the previous category where we just serve the bundle to the client, but it has since gained the ability to use a WebSocket connection similar to LiveView via SignalR.
  • Also available for other compiled languages:

And that's like... it. This is actually really difficult to replicate in other popular web languages like PHP or Ruby. Because the communication channel must be stateful, so it is very resource heavy to scale. If you are not naturally positioned for concurrency like on Go or BEAM or just compiled like Rust or SignalR on .NET CLR, it's not really feasible to do this over a WebSocket. There are attempts with AJAX/polling instead, notably Laravel Livewire. HTML-based HATEOAS is an even cruder option in this same category, here the most famous option is HTMX. They list a number of options in this category here as well.

The elephant in the room

While we weren't paying attention, JavaScript has taken over the world. I've mostly avoided talking about server-side JavaScript frameworks thus far, but JavaScript is native to HTML as well. So, just write JavaScript? It's worth re-linking to some of the same posts in the 2017 survey, although the situation has changed considerably:

Precursor: Meteor

There was a point in time when Meteor was a big thing. It used famous NoSQL database MongoDB and a pub-sub system to push updates across the server-client boundary. Back then it really didn't scale well (see previous discussion about LiveView) and is now mostly forgotten. But it showed that an integrated environment was possible, although entirely in JavaScript still at this point. Perhaps the issues have been fixed by now? It's likely a little too late.

Detour: Isomorphic code

I don't like this word really because it's not what an isomorphism is. Apparently the word came in 2012 and Airbnb wrote about it. The article mentions Meteor, but I don't really think people think about this concept that way anymore. It describes JavaScript code written that can be run both in the browser as well as on the server (not necessarily always with the same behaviour, though; most definitions allow the code to take on different code paths depending on the execution context).

The mainstream frameworks: Remote functions

The mainstream web frontend frameworks like React, Vue, Svelte, etc., all come with a corresponding "backend metaframework" -- respectively, Next/Remix for React, Nuxt for Vue, SvelteKit for Svelte. These metaframeworks are roughly just regular backend JavaScript frameworks that integrate with the respective frontend framework for routing and templating. Because the frontend frameworks themselves are so similar already, the metaframeworks also converged on a similar design to solve this problem: remote functions. They roughly work like this:

  • Write a regular function like you would in any JavaScript backend.
  • Provide invisible plumbing necessary to call said function from the browser by serializing the input/output interface of the function into an HTTP endpoint, and wrap all callsites on the frontend to route through the HTTP endpoint.

The result is that you write code where it just looks like you are calling a function, but the runtime is making the server round-trip for you, should you choose to call that function on the frontend. (You can just continue the function on the backend as usual without any overhead.)
It's still RPC, but the details are just abstracted away for you. It's actually most similar to the ColdFusion transparent remote calls. You can kind of see these as a multi-tier system with manual annotations.

I'm interested in trying out some of these (especially non-JS) systems at some point, just to see what it's like. I'm also curious what this forum thinks about this sort of thing.


[1] Laure Philips, Coen De Roover, Tom Van Cutsem, and Wolfgang De Meuter. 2014. Towards Tierless Web Development without Tierless Languages. In Proceedings of the 2014 ACM International Symposium on New Ideas, New Paradigms, and Reflections on Programming & Software (Onward! 2014). Association for Computing Machinery, New York, NY, USA, 69–81. https://doi.org/10.1145/2661136.2661146

See you soon!
#21727

There is a question of whether you'd even want to do any of this in the first place, wasn't really the point of this post I think. But I think the ColdFusion transparent remote function call-style abstraction is probably fine because it's very thin, very obvious what the code turns into, and likely something you would do yourself anyway. With automatic splitting we can have doubts about the bundle size and so on.

See you soon!
#21728

Entirely forgot about runtimes that render into the WebAssembly+canvas combo... There's a lot of these now too (more often game engines rather than toolkits), very rare in 2017 I imagine. You can usually choose whatever language you want with these provided the necessary bindings. Since Qt has this target now I'm not sure if Wt as mentioned by yorhel is very relevant anymore.

See you soon!