The Elm Architecture was always, at its core, a server-side pattern running in the wrong place.
Model, update, view. Immutable state. Pure functions. Unidirectional data flow. Explicit side effects as values. These are the same patterns that structured server-side web development before JavaScript frameworks decided to absorb the entire application into the browser. When Chris McCord built Phoenix LiveView, he explicitly cited Elm as the model; the difference was putting the state machine back on the server where it arguably belongs.
Sky, an experimental language by Andrea Anzellai, follows this thread to its logical conclusion. It takes Elm’s syntax and architectural conventions, runs them through Hindley-Milner type inference, and emits Go source code. The output is a single, statically linked binary that serves a server-driven UI. No JavaScript framework, no Node.js runtime, no deployment ceremony. This is a small project at an early stage, but the design concept is precise enough to examine carefully.
The Hindley-Milner Layer
The type system is the first place to linger. Hindley-Milner, formalized by Damas and Milner in 1982, gives you complete type inference: you write no type annotations, and the compiler infers the most general type for every expression using a unification algorithm (Algorithm W). The system is sound and decidable, meaning inference always terminates and type errors are genuine compile-time guarantees.
Elm’s variant is particularly clean because of a deliberate decision to exclude type classes. Haskell extends HM with type classes to enable ad-hoc polymorphism, which is powerful but introduces notoriously confusing error messages involving overlapping instances and ambiguous type variables. Elm keeps the base HM system and nothing more. The trade-off is less abstraction flexibility, but error messages that reliably identify the actual problem.
When a language compiles to Go, keeping HM inference at the source level still matters. Go has a type system, but it requires explicit annotations throughout. An HM frontend lets you write in an Elm-style where types flow through function boundaries without annotation, and then emit the fully annotated Go that the Go compiler needs downstream. The developer experience lives in the source language; the Go output is an artifact.
The Server-Driven UI Architecture
Server-driven UI is not a new pattern. The browser-as-thin-client model predates single-page applications by decades. What is new is the set of tools that have made it competitive with SPAs on interactivity: WebSockets for bidirectional communication, efficient DOM-patching libraries, and frameworks that manage the state synchronization protocol automatically.
Phoenix LiveView, built on Elixir and OTP, is the most technically refined implementation. Each LiveView component is an Erlang process holding state in memory. When a user clicks a button, the browser sends the event over a WebSocket; the server calls handle_event, produces a new state, calls render, diffs the result against the previous render, and sends back a binary patch. The client applies the patch using morphdom. The developer never writes the synchronization protocol by hand.
def mount(_params, _session, socket) do
{:ok, assign(socket, count: 0)}
end
def handle_event("increment", _params, socket) do
{:noreply, update(socket, :count, &(&1 + 1))}
end
def render(assigns) do
~H"""
<button phx-click="increment">+</button>
<p>Count: <%= @count %></p>
"""
end
The structural correspondence to Elm is exact. mount is init. handle_event is update. render is view. LiveView makes this mapping intentional and explicit; McCord has described Elm as a direct influence on the design.
Livewire (for PHP/Laravel), Blazor Server (for .NET), and HTMX (language-agnostic) fill the same space with different trade-offs. HTMX in particular requires no framework at all on the server side: you handle HTTP requests, return HTML fragments, and HTMX patches the DOM. But it sacrifices the type safety and structured state management that LiveView enforces.
Sky’s position in this landscape is interesting precisely because it targets Go rather than Erlang, PHP, or .NET. Go is not typically associated with functional programming or type-safe UI, but it is extraordinarily good at the specific deployment problem: cross-compilation, static linking, minimal container images, and a standard library that includes a production-capable HTTP server. GOOS=linux GOARCH=amd64 go build produces a self-contained binary from any development machine. That binary runs on a bare Linux host with no runtime installed, fits inside a FROM scratch Docker image, and can be deployed with a single file copy. That simplicity is real and undervalued.
What the Compilation Pipeline Does
When Sky emits Go, it is doing roughly this mapping in code:
A Sky Model type becomes a Go struct. A Sky Msg union type becomes a Go interface or tagged struct, since Go lacks native sum types. The update function becomes a Go function that switches over message variants. The view function emits HTML using Go’s html/template package or a custom builder.
-- Sky source (Elm-inspired)
type alias Model = { count : Int }
type Msg = Increment | Decrement
update : Msg -> Model -> Model
update msg model =
case msg of
Increment -> { model | count = model.count + 1 }
Decrement -> { model | count = model.count - 1 }
The emitted Go handles the struct definitions, the HTTP handler wiring, and the template rendering. The Go toolchain then handles everything downstream: binary compilation, cross-compilation, and linking.
This mirrors what Elm itself does, except Elm targets JavaScript. The Elm compiler parses source, runs HM inference, produces a typed AST, and emits JavaScript. Sky replaces the JavaScript emission step with Go emission. The frontend language stays expressive and safe; the backend gains Go’s deployment characteristics.
The significant design question is concurrency. Erlang (LiveView’s foundation) gives cheap process isolation per session via the BEAM scheduler: millions of concurrent LiveView sessions each in their own process is a realistic production deployment. Go can handle high concurrency via goroutines, but session isolation looks different. You are likely working with HTTP sessions, WebSocket handlers, and in-process state, which requires careful design around concurrent access. How Sky’s compiled output models this per-session state is a question the project will have to answer clearly as it matures.
Where This Sits in the Ecosystem
There is a persistent category of language projects that bring ML-family type systems to environments dominated by weaker ones. ReScript compiles OCaml-derived syntax to JavaScript. GopherJS compiles Go to JavaScript for the browser, the inverse of Sky’s direction. PureScript compiles a Haskell-like language to JavaScript. Gleam compiles a statically typed language to Erlang or JavaScript. Each of these is making a bet: the source language’s type system is worth the compilation step.
Sky is working the same seam from a different angle. Take the cleanest, most principled frontend language architecture, transplant its type system and application model, and route the output to Go for the deployment story rather than the browser. The value proposition is coherent. Go’s single-binary deployment is genuinely excellent. Elm’s architecture and type system are genuinely excellent. The question is whether a source-to-source compiler across that conceptual gap can hold together ergonomically, or whether the seams become visible in ways that cost more than they save.
Projects like Sky tend to quietly fade if the author’s interests shift, or become the basis for something more mature when someone with sustained motivation picks them up. The Elm community has discussed what a server-side Elm would look like for years; there is real appetite for the idea. Go is not the obvious answer to that question, but the more you think through the pairing, the more sense it makes: HM types for the application logic, Go’s compilation model for the binary. The design is worth watching.