· 8 min read ·

Sky: Elm's Architecture, Go's Deployment Story, and Why the Pairing Makes Sense

Source: lobsters

There is a small but persistent tradition in language design of taking a good idea from one ecosystem and transplanting it somewhere more pragmatic. ReScript took OCaml’s type system to the JavaScript world. Gleam brought Hindley-Milner inference to the Erlang VM. Fable carried F# semantics to the browser via JavaScript. Each of these is a bet that the right type system, married to the right runtime, can produce something more useful than either alone.

Sky is the latest entry in this tradition. It is an Elm-inspired language with Hindley-Milner type inference that compiles to Go, with server-driven UI as its primary deployment model and single binary output as its packaging story. That is a lot of ideas in one sentence, so it is worth unpacking each one to understand why they fit together.

What Elm Actually Got Right

Elm’s lasting contribution to web development is not its syntax. It is The Elm Architecture (TEA), a strict unidirectional data flow model built on three concepts: a Model that represents all application state, an Update function that takes a Msg and the current Model and returns a new Model, and a View function that takes the Model and returns a description of the UI. Side effects are quarantined into Cmd values, which the Elm runtime executes outside the pure core.

This architecture eliminates an entire class of bugs. There is no mutable shared state, no implicit event handler side effects, no surprising re-renders caused by lifecycle method interactions. The runtime can reproduce any application state deterministically given the initial state and the sequence of messages that arrived. This is why Elm’s famous “no runtime exceptions” claim holds in practice, not just in theory.

The limitation is that Elm was designed for the browser. Its runtime is JavaScript. Its ecosystem is client-side. The moment you need a server, you are back in Node.js or writing a separate backend in another language entirely. TEA’s clarity does not extend to the server because TEA was never meant to run there.

Sky’s premise is that this constraint is not fundamental. The architecture works. The compilation target is arbitrary.

Hindley-Milner: Complete Inference Without the Annotations

The type system Sky draws from is Hindley-Milner, the inference algorithm at the heart of ML, Haskell, OCaml, and Elm itself. The core insight of HM is that a type checker can recover complete type information for any well-typed program without requiring the programmer to write a single type annotation. This is not the same as TypeScript’s inference, which fills in types where it can but relies on explicit annotations for anything non-trivial.

HM inference works through unification. When you write let add x y = x + y, the checker generates fresh type variables for x and y, constrains them by the fact that + requires numeric arguments, and resolves those variables to a concrete type. The result is a polymorphic function with a fully inferred signature: add : number -> number -> number.

What makes HM particularly powerful is let-polymorphism: values bound with let get generalized automatically. The function identity x = x becomes forall a. a -> a without any annotation, and every use site gets the specific type it needs. This is fundamentally different from languages where you need to write <T>(x: T): T => and still worry about whether inference propagates correctly downstream.

For a language targeting server-side development, a complete inference algorithm matters because server code is often more structurally complex than UI code. Handler functions compose, middleware wraps handlers, configuration flows through multiple layers. In a language with incomplete inference, each of those layers requires annotations to avoid inference failures. In HM, the types flow through automatically.

Server-Driven UI: The Server as the Source of Truth

Server-driven UI (SDUI) is an architectural pattern where the server does not just send data; it sends a description of the interface itself. The client is a renderer, not a decision-maker. Instead of the client fetching a list of items and deciding how to present them, the server sends a component tree: a button here, a list there, each component described by type and properties.

This pattern has been deployed at scale by companies including Airbnb, which built its Ghost Platform on SDUI principles, and Lyft, which uses it to update ride-experience screens without requiring an app store release cycle. The core advantage is that the server controls what users see in real time. A product change that previously required a client deployment becomes a server configuration change.

The tradeoffs are real. Every interaction now has a network round trip in the critical path unless you build sophisticated prefetching. The schema describing UI components needs careful versioning, because old clients and new servers will coexist in production. The client renderer must be expressive enough to handle any component the server might send, which creates its own maintenance surface.

Where SDUI connects naturally to Elm’s architecture is in the model of truth. In TEA, the Model is authoritative. Everything the view renders is derived from it. In SDUI, the server is authoritative. Everything the client renders is derived from it. They share a structural principle: the UI is a pure function of some canonical source of state, and mutation flows through a single controlled path.

When the language itself compiles to server code, the connection becomes tighter. The same type system that governs your domain logic can govern the component descriptions you send to the client. There is no impedance mismatch between the application’s internal representation and the wire format, because both are expressed in the same language.

Why Go as a Compilation Target

The choice of Go as a compilation target is more interesting than it might appear. Go is not the obvious choice for a functional language’s backend. It is imperative, it has no algebraic data types in the language itself, its error handling is explicit rather than monadic, and its support for higher-kinded types is nonexistent.

But Go has properties that matter enormously for a language targeting deployment:

Static binaries. A Go program compiled with CGO_ENABLED=0 produces a single self-contained executable with no external runtime dependencies. No JVM, no Node.js, no Python interpreter. You copy one file and it runs. For server-side code this matters at the operational level. Container images stay small. Deployments are atomic. There is no dependency resolution at startup.

Fast compilation. Go’s compilation is notoriously quick. A transpiler that emits Go code can therefore offer short iteration loops, which is important for a language still developing its tooling. The Go compiler becomes Sky’s backend without Sky needing to build one.

Mature standard library. Go ships with production-quality HTTP servers, TLS, JSON, SQL drivers, and concurrency primitives. A language that compiles to Go inherits all of that for free. Writing a full HTTP server that handles TLS, graceful shutdown, and concurrent connections requires essentially no dependencies in Go.

Goroutines. Go’s lightweight concurrency model maps well to server-side event handling. A web server that spawns a goroutine per request and communicates over channels is idiomatic Go. If Sky’s compiled output uses goroutines for effects, it inherits a battle-tested concurrency model without implementing one.

Other languages have targeted Go as a compilation backend, though the tradition is thin. The more common direction is Go interoperating with C via CGO. But the tooling path Sky takes, emitting Go source that the standard go build can compile, is straightforward to maintain and produces output that Go developers can read and debug if needed.

Compare this to compiling to C, which requires handling memory manually and producing correct, safe output under all circumstances. Or compiling to LLVM IR, which requires understanding a complex and evolving intermediate representation. Emitting Go is high-level enough to be tractable and low-level enough to be performant.

The Single Binary Promise

The “single binary output” feature is ultimately what makes Sky’s stack coherent as a deployment story. You write functional, type-safe code in a language with complete inference and a clear separation of effects. That code compiles to Go. Go compiles to a binary. That binary is your server.

This compares favorably to what a Node.js stack looks like in production: a JavaScript bundle, a node_modules directory potentially containing thousands of packages, a Node.js runtime, and possibly a separate build step to produce that bundle. Or a JVM stack: a .jar file, a Java runtime, heap size configuration, GC tuning. The operational simplicity of a self-contained binary is a genuine advantage that production engineers appreciate immediately.

For server-driven UI specifically, this matters because the server is the critical path for every user interaction. Simpler deployment means fewer failure modes. A binary that starts in milliseconds means faster cold starts in containerized environments. A server with no dependency on an external package registry at runtime means fewer supply chain vectors.

Where This Fits in the Broader Landscape

Sky sits in a small but growing space of languages that take functional type system ideas seriously and target server-side deployment. Gleam is probably the most mature comparison: a Hindley-Milner language with algebraic data types that compiles to Erlang and JavaScript, targeting the BEAM runtime for its fault tolerance and concurrency story. Gleam ships a single binary CLI and produces output that runs on an existing virtual machine.

The difference is that Go’s deployment model is arguably simpler than the BEAM’s. The BEAM is excellent at fault tolerance and distributed systems, but it is a runtime with its own operational characteristics. A statically compiled Go binary has fewer moving parts at runtime.

The Ur/Web project explored a similar space from a different direction: a functional language for full-stack web applications that compiled both client and server code from a single source, with type-safe database queries included. Ur/Web was rigorous and complete but demanding to use, and its tooling never reached the ergonomic threshold for wide adoption.

Sky appears to be targeting a more practical middle ground. Elm’s architecture is familiar to a generation of frontend developers. Go is familiar to a large portion of backend developers. A language that bridges both, with modern type inference and a clean deployment story, has a reasonable case to make.

What to Watch

The project is early. The GitHub repository is sparse on documentation, which means its claims are not yet fully verifiable against working examples. The hardest parts of this kind of project are typically not the happy path but the edges: how does it handle recursive types, how does the SDUI component schema evolve across versions, how does the HTTP server integration work, what happens when you need to escape to raw Go for a library that has no Sky equivalent.

Hindley-Milner inference is well-understood algorithmically, but implementing it correctly with good error messages is a substantial engineering project. Elm’s type error messages are famously helpful precisely because the Elm team invested years in making the error reporting informative. A new language reaching that level of quality requires sustained effort.

But the architectural choices here are sound. Compiling a functional language with complete type inference to Go, targeting server-driven UI, and delivering a single binary is a coherent set of ideas. Each choice reinforces the others. The type system keeps the server code honest. The SDUI pattern keeps the client-server contract explicit. Go’s compilation model keeps deployment simple.

That coherence is worth paying attention to, even in an early project.

Was this interesting?