· 6 min read ·

Sky and the Case for Compiling Elm's Architecture to Go

Source: lobsters

The Sky language does something worth paying attention to: it takes the core ideas from Elm, strips out the browser-JavaScript assumption, and compiles to Go instead. The result is a language with Hindley-Milner type inference, a server-driven UI model, and single binary output. That combination is unusual enough to be interesting even if the project never ships 1.0.

To understand why, it helps to look at each of those three properties separately and ask what they mean when they land in Go’s ecosystem.

The Elm Architecture, Server-Side

The Elm Architecture (TEA) is a pattern for structuring UI applications as a triple: a Model that holds all application state, an update function that takes a message and the current model and produces a new model, and a view function that renders the model to some output. The key property is that data flows in one direction. Messages come in, state changes, the view re-renders from scratch. There are no mutation, no callbacks buried inside component trees, no shared mutable state.

Elm makes this work in the browser by compiling to JavaScript and maintaining a virtual DOM diff. But the architectural pattern itself does not depend on the browser. The unidirectional flow and the separation between state and rendering are just good software structure.

Server-driven UI picks up exactly this idea. Instead of shipping a JavaScript application to the browser and letting it manage state, the server owns the state and pushes HTML (or patches to the DOM) over a persistent connection. Phoenix LiveView is the most mature implementation of this model: the server holds a process per connected client, each process runs its own handle_event function (structurally similar to Elm’s update), and it diffs the rendered template on state change and sends minimal patches over WebSocket. The browser side is a thin JS shim that applies those patches.

HTMX takes a lighter approach, letting any HTML element trigger HTTP requests and swap in the response. Less stateful, more hypermedia-native, but the philosophical commitment is similar: the server is authoritative.

Sky applies TEA directly to this server-driven model. The view function produces something the server renders and sends, not something that runs in a browser runtime. Go handles the networking, the concurrency, the HTTP or WebSocket layer. The language you write in is the Elm-flavored functional layer on top.

Hindley-Milner in a Go World

Go’s type system is explicit and structural. You declare types, the compiler checks them, and inference is limited to short variable declarations with :=. There is no global type inference. This is intentional: Go’s designers have consistently prioritized readability and explicitness over ergonomic brevity.

Hindley-Milner type inference, the algorithm underlying Haskell, OCaml, Elm, and ML family languages, takes the opposite position. You write almost no type annotations, and the compiler infers the most general type for every expression through unification. A function that happens to work on any comparable type gets a type variable rather than forcing you to commit to int or string. The system is complete: if a program type-checks, it has a unique principal type.

The practical payoff in Elm is that you write functions like:

List.map (\x -> x * 2) [1, 2, 3]

and the compiler figures out x is Int, the list is List Int, the output is List Int, without any annotations. In a more complex program, the same inference propagates through dozens of composed functions. You add a type annotation when you want documentation, not because the compiler needs it.

Bringing HM inference to a Go-targeting language means the compiler generates Go with explicit types even when the source has none. The Sky compiler writes the int64 or the []string that Go’s type system requires; the programmer writes the abstract, type-variable version. This is similar to what languages like Gleam do on the Erlang/Elixir runtime: the source language is fully inferred, the target runtime is typed differently, and the compiler bridges the gap.

The tension is real. Go’s generated code will be verbose. If you read the output of the Sky compiler, you will see explicit types everywhere, because that is what Go requires. The ergonomics of Sky exist only at the source level. Whether that is a good tradeoff depends on whether you ever need to read or debug the generated Go, and whether the Go ecosystem interoperability is worth the abstraction cost.

Single Binary Output

Go’s single binary story is one of its most practically valuable properties. You run go build, you get a statically linked executable, you copy it to a server, it runs. No runtime installation, no dependency resolution at deploy time, no shared library conflicts.

Sky preserves this by compiling to Go rather than, say, running its own interpreter. The output is idiomatic enough Go that the standard go build toolchain takes over from there. This is a meaningful design choice. A language that compiles to its own bytecode or requires its own runtime gives up one of the main reasons to target Go at all.

For contrast, languages like GopherJS went the other direction: Go source that compiles to JavaScript. Sky flips that arrow. The source is a high-level functional language, the output is Go, and the distribution artifact is a single binary. The Go standard library, the Go HTTP stack, the Go concurrency primitives, all of that is available downstream of the compilation.

The Design Space This Opens

What Sky represents is a bet that the functional architecture of Elm and the operational profile of Go are a natural fit, and that the mismatch is only at the language level. Go programs that implement server-driven UI exist today. Phoenix LiveView’s model is reproducible in Go with goroutines per session and WebSocket connections. Some teams have built exactly this. But they write it in Go, which means managing the state machine manually, wiring up handlers by hand, and relying on discipline rather than the type system to keep the update function pure.

A language that makes the TEA structure the only structure, enforced by a type system with full inference, removes an entire class of architectural errors. You cannot accidentally mutate global state from inside a view function if the language does not give you a way to do that. You cannot forget to handle a message variant if the pattern match is exhaustive by construction.

The prior art in this space is thin. Nickel applies gradual typing to Nix’s configuration language. Elvish brings a structured data model to shell scripting. These are projects that take a well-understood language category and improve the type story, but they do not compile to another typed language. Sky’s compilation target is unusual: it uses Go’s type system as a backstop for its own, which is a layered approach with potential for both soundness and confusion.

The Honest Tradeoffs

The project is early. The GitHub repository has the shape of an experimental language rather than a production toolchain. That is not a criticism; all languages start somewhere, and the ideas here are coherent enough to warrant building a prototype against.

The harder question is what the interoperability story looks like. If you want to call a Go library from Sky, or expose Sky functions to existing Go code, the compilation model needs a clear answer. Languages that compile to another language tend to hit this wall: the source language’s semantics and the target language’s idioms diverge in ways that make the bridge awkward. Elm handled this by having ports, a structured message-passing boundary between Elm and JavaScript. Sky will need something similar for Go.

The server-driven UI model also carries its own tradeoffs. Phoenix LiveView is mature enough to have documented failure modes: latency on high-interaction UIs, reconnection handling, state synchronization on page refresh. A Go implementation with a functional source language inherits those problems at the architecture level, not at the library level. The language cannot solve network partitions.

Still, the combination is interesting. Elm demonstrated that the functional architecture produces reliable, maintainable UI code. Go demonstrated that server-side simplicity compounds over time. Sky is asking whether those two lessons can be combined into a single tool, and the answer is worth finding out.

Was this interesting?