An overview of (functional) Front-end free development technologies

7 July 2023

In search of web programming that doesn’t feel like javascript.

Picture of the web

Making web apps is the easiest way to reach people. However it comes with a price: JavaScript, with its peculiar semantics and constantly changing ecosystem.

Frontend development has a bad history of managing the complexities of user interaction, asynchronicity, and styling on top of the application logic. Mixing these concerns makes it hard to reason about the code, and to maintain it. To avoid this a lot of the JS community settled on using a number of frameworks that provide a definite abstraction to provide a canonical architecture in an opinionated way, like React, Vue, Svelte, etc.

But at the end of the day, that is still JavaScript. So, I wanted to explore the different possibilities that I feel would be fun to work with for frontend development. I leave the motivation for it to the end as it’s not the main point, but ideally a frontend stack should be able to also run fullstack.

There are a number of interesting approaches available, which I would classify in three categories:

And there are many more that I will only briefly mention in a last section; notably WebAssembly, making possible to use languages such as Rust.

Backend-only

These approaches have a feeling of only having a backend. Of course there is still a need to write pages, but this does not create additional layers to handle.

Liveview

Liveview is a technology that allows us to write web apps entirely in the server, and through a constant websocket connection with the client, updating the DOM in real time. This approach avoids some of the common issues with SPAs, as it takes advantage of browser routing and server-side rendering, avoiding to depend on large assets bundle download. There is no need for client-side state management, there isn’t any need to develop an API.

The downside is that for high-interactivity websites the experiences may be bad in poor network conditions (and that it isn’t suitable for serverless cloud platforms).

The reference implementation is the Phoenix framework, which made this approach possible by leveraging the high concurrency of Elixir. There are also implementations in other languages: Complete list of Liveviews implementations

Here is the code example from Phoenix’s documentation:

defmodule TimelineLive do
  use Phoenix.LiveView

  def render(assigns) do
    render("timeline.html", assigns)
  end

  def mount(_, socket) do
    Twitter.subscribe("elixirphoenix")
    {:ok, assign(socket, :tweets, [])}
  end

  def handle_info({:new, tweet}, socket) do
    {:noreply,
     update(socket, :tweets, fn tweets ->
       Enum.take([tweet | tweets], 10)
     end)}
  end
end

It’s more impressive on the homepage with the live demo on the side though :-)

Htmx

The idea is to extend the HTML syntax to be able to specify API calls and DOM updates. If you ever wondered why modern websites fight web standards instead of embracing them, this is the solution for you. This basically feels like developing a static website following all Html standards. The only added work is that the server needs to generate the Html fragments, so the server API may be provided with a translation layer from Json to Html.

Because the approach is so simple, it is available in many languages and frameworks: Awesome Htmx list

Here is an example taken directly from the documentation::

<button hx-post="/clicked"
    hx-trigger="click"
    hx-target="#parent-div"
    hx-swap="outerHTML"
>
    Click Me!
</button>

This tells htmx:

When a user clicks on this button, issue an HTTP POST request to ‘/clicked’ and use the content from the response to replace the element with the id parent-div in the DOM.

Honestly, I initially thought Html5 was going to be exactly this, providing enough basic interaction that standard websites would require no JavaScript.

There are other frameworks with a fairly similar approach, in particular Unpoly and Hotwire. There are more comparisons between the three that can be found, but this falls outside the scope of this article since it would need to be much more in-depth to be useful (for example, Htmx does provide a guide to migrate from Hotwire to Htmx).

Homogeneous stack

These languages provide a more classical approach to frontend development, but with the ability to run both on the server and on the client, with some added bells and whistles (immutability or higher-kinded types). They provide JS interoperability, providing a way to use the existing ecosystem.

Clojure(Script)

Clojure is probably one of the most obvious choices in that space, but it’s not easy to get started with. In particular it has a plethora of web frameworks, which makes the initial choice harder to make. But it means it’s possible to choose the one that fits the project best. There are more ‘fullstack’ approaches like Coast, and some that are there to provide a simple compositional glue between specialized libraries. In no particular order, here are some of great projects:

A more complete list of resources can be found on this awesome list.

Of note is reagent, which is a wrapper around React, and re-frame, a framework built around it to simplify SPA development.

It’s harder to find a really representative code example given all the possible approaches, but here is the one from Coast, which should at least convey its lispiness:

(ns server
  (:require [coast]))

(defn home [request]
  (coast/ok "You're coasting on clojure"))

(def routes (coast/routes [:get "/" home]))

(def app (coast/app {:routes routes}))

(coast/server app {:port 1337})

A very interesting fullstack framework is Electric, which description is ‘a reactive DSL for full-stack web development, with compiler-managed frontend/backend network sync’. Released in alpha this year, it’s a quite recent project which might go into a category of it own as many of the ideas are quite novel and do not have real equivalent.

F#

F# is a fairly modern functional language in the .NET ecosystem.

In particular there is a framework called Elmish that provides an Elm-_ish_ experience (Elm being covered below).

To give a taste of it, here is a snippet from the Elmish documentation:

let view model dispatch =

    div []
        [
            button [ OnClick (fun _ -> dispatch Decrement) ] [ str "-" ]
            div [] [ str (sprintf "%A" model) ]
            button [ OnClick (fun _ -> dispatch Increment) ] [ str "+" ]
        ]

The Increment and Decrement are simply instances of a Msg type, and it is sufficient to implement an update function that takes the message as argument and does the expected.

There are other frameworks recommended on their main website, like WebSharper and SAFE.

PureScript

PureScript is basically a Haskell for the web (without the laziness). There is a page to describe the main differences with it.

The documentation feels very Haskelly, with less emphasis on building web apps and more on Type Classes and Monads than the others. The Halogen documentation provides a more accessible introduction, with an example that is not very different from the F#/Elm one; in fact it also a fullstack framework that aims for an Elm-_ish_ experience.

module Main where

import Prelude

import Effect (Effect)
import Halogen as H
import Halogen.Aff as HA
import Halogen.HTML as HH
import Halogen.HTML.Events as HE
import Halogen.VDom.Driver (runUI)

main :: Effect Unit
main = HA.runHalogenAff do
  body <- HA.awaitBody
  runUI component unit body

data Action = Increment | Decrement

component =
  H.mkComponent
    { initialState
    , render
    , eval: H.mkEval $ H.defaultEval { handleAction = handleAction }
    }
  where
  initialState _ = 0

  render state =
    HH.div_
      [ HH.button [ HE.onClick \_ -> Decrement ] [ HH.text "-" ]
      , HH.div_ [ HH.text $ show state ]
      , HH.button [ HE.onClick \_ -> Increment ] [ HH.text "+" ]
      ]

  handleAction = case _ of
    Increment -> H.modify_ \state -> state + 1
    Decrement -> H.modify_ \state -> state - 1

There are some alternatives such as Flame, and Concur; more can be found on this awesome list of resources.

ReScript

ReScript is a language that suffers from a confusing branding (with BuckleScript, Reason, ReasonML often ending being confused). It is a language that wants to combine the best of JavaScript and OCaml, i.e. a familiar syntax with safe algebraic datatypes.

module Button = {
  @react.component
  let make = (~count: int) => {
    let times = switch count {
    | 1 => "once"
    | 2 => "twice"
    | n => Belt.Int.toString(n) ++ " times"
    }
    let msg = "Click me " ++ times

    <button> {msg->React.string} </button>
  }
}

Web-oriented languages

These languages go beyond providing Html as an embedded DSL and provide full support of web features in a natural way and a full stack framework.

Elm

Elm wants to be a “A delightful language for reliable web applications”. The quick sample would be almost identical to the one from the F# section above.

Unfortunately, I probably learnt about it through one of its controversies, revolving around some of its design choices and community (or ‘walled garden’) management. The consensus seems to be that it is a real dream to work with, but can become a real nightmare in case the language doesn’t provide a feature you need. Another common complaint is that it requires a lot of boilerplate to do simple things.

One particular post is Elm 0.19 broke us; a way less popular post is Life after Elm, which describes a happy migration to F#.

Elm seems to have introduced a lot of really great ideas given the number of Elm-_ish_ frameworks in other languages.

Imba

Imba is a very practical and expressive language. The main example from the website is a small but impressive drawing app1:

import './canvas'
import './pickers'

global css body m:0 p:0 rd:lg bg:yellow1 of:hidden

const strokes = [1,2,3,5,8,12]
const colors = ['#F59E0B','#10B981','#3B82F6','#8B5CF6']
const state = {stroke: 5, color: '#3B82F6'}

tag App
	<self>
		<div[ta:center pt:20 o:0.2 fs:xl]> 'draw here'
		<app-canvas[pos:abs inset:0] state=state>
		<div.tools[pos:abs b:0 w:100% d:hgrid ja:center]>
			<stroke-picker options=strokes bind=state.stroke>
			<color-picker options=colors bind=state.color>

imba.mount <App[pos:abs inset:0]>

The other examples are also very impressive, and the language seems to be very well thought out. No fancy monoidal endormorphic functors or anything of the sort, but a very practical and expressive language. Tag closing based on indentation is a nice little touch to keep a familiar syntax without the verbosity of XML.

Miscellanea

I almost put Marko in the third category, as an approach somewhat dual to Htmx, i.e. what if we could put more code in the templates? The result is fairly elegant and concise, but this is really in ‘JS framework’ territory.

There are other approaches that fall outside of the scope of this research, which tend to have either broader scope (become a universal application toolkit) or a more specific one (build a data visualization app in minutes). Some examples would be Haxe or Google’s Flutter.

The introduction of WebAssembly/Emscripten has also opened the door to a number of other languages, like Rust. In particular there is a number of Technologies based on existing ‘desktop’ stacks, like JavaFX, Microsoft’s Blazor, based on C# and .NET. The Python ecosystem has Brython, Pyodide, Pynecone, Flet, and others.

There is also a full graveyard of failed projects in that space, or that failed to take off in any significant way, despite being technically impressive. Choosing one may be a bet with the time investment these technologies require.

Appendix: some issues with standard web development

Aside from the complexity introduced by the tooling, which may be fine in big structures where this forces different teams to structure their processes, there are some issues with a dual language approach2.

For example, frontend and backend both have a DSL to perform query filters –most probably different, unless the FE simply sends serialized SQL queries, which is not something I’ve ever seen advised. Then the bulk of the API endpoints code consisting in mapping the two, with a considerable fraction that is focused in handling edge cases (e.g. null values) to the appropriate behaviour (update, leave unchanged), on a case by case basis. Therefore bugs tend to be copy-pasted from one endpoint to another until they are fixed in some specific cases.

When there is a need for some complex data structures, the serialization/deserialization code needs to be implemented twice, with a risk of bug mismatch. Validation is also a problem, as it needs to be implemented twice, or requires a server round-trip. In some cases the serialization may need an additional simplification step that requires some complex algorithm; in that case, the FE does a naive serialization then waits for the server to send the simplified version. So code duplication and round trips are both needed.

The main factor is that it is time-consuming to have an entirely different stack to work with; yet even absent of that there would be significant issues with that approach.

Webmaking

  1. The syntax highlighting is a bit iffy since rouge does not support Imba nor Marko, so it’s using Javascript. 

  2. Although natural languages analogies are quite tenuous, I wonder if someone would recommend having one team of only say Spanish-speaking people and the other Chinese-speaking. In all cases that I’ve known, everyone prefers to settle on (maybe poor) English as a common language.