· 6 min read ·

The Elm Architecture Was Always a Better Fit for Servers

Source: lobsters

There is a project on GitHub called Sky that describes itself as an Elm-inspired language with Hindley-Milner type inference, server-driven UI output, and single binary compilation via Go. On the surface it reads like a list of appealing buzzwords. Spend a few minutes with the design and a more interesting thesis emerges: The Elm Architecture, as a computational model, fits a stateless HTTP server more naturally than it fits a browser runtime. Sky is an attempt to build on that observation.

What The Elm Architecture Actually Is

Elm’s architecture (TEA) is often described as a state management pattern, but that undersells its precision. TEA defines a program as three pure functions:

init   : Flags -> (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view   : Model -> Html Msg

The runtime holds the Model, feeds events as Msg values to update, applies the resulting new state, and calls view to produce a description of the UI. Side effects are never performed directly inside these functions. Instead, Cmd values are returned and the runtime executes them, feeding their results back as new Msg values. The program is a pure state machine; the runtime is an interpreter.

This pattern, introduced by Evan Czaplewski in 2012, directly influenced Redux, Vuex, and most of the major frontend state management libraries that followed. But its influence was always a little awkward in the browser context, because the browser is full of mutable state, DOM APIs, and imperative callbacks that have to be wrapped and funneled through the Cmd/Sub system. The architecture imposes purity on an inherently impure environment.

Now consider what an HTTP server does. It receives a request (a Msg). It may load or read some state (a Model). It runs some logic (an update-like function). It returns a response (the result of view). Side effects (database writes, outbound HTTP) happen at the edges. The request-response cycle is, structurally, one iteration of the TEA update loop.

Sky takes this observation and builds a language around it.

Hindley-Milner in a Go World

The type system choice is significant. Hindley-Milner inference, developed independently by Roger Hindley in 1969 and Robin Milner in 1978 (and formalized as Algorithm W by Damas and Milner in 1982), allows a compiler to infer the most general possible type for every expression without requiring annotations. Languages like OCaml, Standard ML, Haskell, and Elm all use variants of it. The key property is principal types: the inferred type is always the most general type consistent with the expression, not just any valid type.

Elm uses a clean version of HM without type classes, which is part of why its error messages are so readable. When inference fails, the compiler knows exactly which constraint could not be unified, and it knows the full type of both sides. Elm’s error messages became a reference point for what good type errors could look like.

Go, the compilation target, does not use HM inference. Go’s type system is structural but nominal, uses interfaces for polymorphism, and prior to version 1.18 had no generics at all. Go 1.18 added bounded parametric polymorphism, which is a different system from HM: you can write func Min[T constraints.Ordered](a, b T) T but you cannot express rank-2 polymorphism, and the type checker does not perform full unification across call sites the way HM does.

This creates a real impedance mismatch. The closest prior project, Oden, attempted to compile a full ML-style language to Go around 2016 and was abandoned. The author’s core problem was that Go’s representation of algebraic data types (essentially interface{} with type assertions, or tagged structs) produced generated code that was difficult to read and even harder to debug. The generated Go was correct but alien, losing the legibility advantage that Go as a target was supposed to provide.

Sky sidesteps part of this by choosing a specific application domain. When your output is a UI description sent over HTTP, you do not need arbitrary algebraic data types in the generated code. You need structs with known shapes, JSON serialization, and HTTP handlers. Go is very good at exactly those things. The HM inference happens entirely at the Sky layer; the generated Go can be relatively straightforward.

Server-Driven UI as the Architectural Lever

Server-driven UI (SDUI) is a pattern where the server response includes not just data but the structure and layout of the interface itself. The client is a generic renderer that maps component descriptions to native implementations. Airbnb wrote about this extensively in the context of their mobile apps, where shipping UI logic in server responses means updating the UI without waiting for App Store reviews. Shopify, DoorDash, and others have adopted similar approaches.

The trade-offs are real. Every interaction may require a round trip. Offline support is harder to implement. Debugging UI problems means reasoning about server responses, not component trees. The client must implement a generic renderer, which is itself a substantial piece of code. Schema versioning becomes a significant operational concern.

But within the TEA model, SDUI is not a hack. The view function in TEA already produces a description of the UI rather than mutating the DOM directly. In browser Elm, that description gets diffed and applied by the runtime. In Sky’s model, that description becomes the HTTP response and the client interprets it. The semantics are the same; only the transport changes. The Model lives on the server. Msg values arrive as HTTP requests. update runs as a handler. view encodes the response. The TEA loop is intact.

This also means the single-binary output from Go is a genuine advantage rather than a curiosity. A Sky application ships as one compiled binary that contains the entire server, all the routing, all the UI logic, and the Go standard library’s HTTP server. No Node.js runtime, no separate asset server, no process manager juggling multiple services. go build handles cross-compilation to Linux, macOS, and Windows from any host. For the kinds of internal tools, admin panels, and lightweight apps where SDUI makes sense, this deployment story is genuinely simpler than the alternatives.

Comparisons Worth Making

The most direct comparison is Phoenix LiveView in Elixir. LiveView maintains a persistent WebSocket connection per session, keeps server-side state, and pushes HTML diffs to the client. The mental model is similar to TEA-on-the-server. The difference is infrastructure: LiveView requires a Phoenix application, an Elixir runtime, and typically a PostgreSQL database for session state at scale. Sky’s single-binary Go output is deliberately simpler, trading LiveView’s real-time push model for standard request-response.

htmx achieves server-driven UI with ordinary HTML and HTTP, letting the server return HTML fragments that the client swaps in. It works with any backend language and requires almost no client-side JavaScript. Sky offers more type safety and a more constrained programming model in exchange for learning a new language. Whether that trade-off is worth it depends on whether the HM type system catches errors that would otherwise reach production.

PureScript is a Haskell-like language with proper HM inference that primarily targets JavaScript. It has had experimental Go backends, but the ecosystem and tooling investment is entirely on the JS side. Sky’s focus on Go and SDUI is a narrower bet, but a more coherent one.

What This Project Is Betting On

Sky is an early project, and experimental language projects carry real risk of abandonment. The prior art (Oden, GoScript, various others) suggests that “ML-inspired language compiling to Go” is a hard problem to sustain. The language ecosystem, tooling, and debugging story all have to be built from scratch.

What makes Sky’s position slightly more defensible than its predecessors is the domain specificity. By committing to the SDUI model, the project constrains what the language needs to express. You are not building a general-purpose ML. You are building a typed description language for server-rendered interfaces, with Go as the execution substrate. The HM inference buys you safety at authoring time. The Go compilation buys you a deployment artifact that operations teams already know how to handle.

The insight that TEA maps cleanly onto HTTP request-response is not a new one, but it has not been packaged as a first-class language before. Whether Sky executes on that insight well enough to develop a real user base is an open question. The design premise, at least, is more coherent than most experimental language projects manage.

Was this interesting?