Elm Architecture on the Server, Compiled to Go: What Sky Is Trying to Do
Source: lobsters
The Sky language surfaced recently on Lobsters with a description that reads like a wishlist: Elm-inspired syntax, Hindley-Milner type inference, server-driven UI, and single binary output via Go compilation. Each of those four properties is interesting on its own. Together, they sketch an approach to application development that borrows from some genuinely good ideas while betting on Go’s deployment story as the foundation.
Let me work through why each piece matters and what it looks like when you combine them.
The Go Runtime Is Worth Compiling To
Before anything else, there is a pragmatic case for targeting Go as a compilation backend. Go compiles to self-contained static binaries with no runtime dependencies. It handles concurrency through goroutines and channels at a level that genuinely scales. Memory usage per goroutine starts around 2KB, which means you can hold tens of thousands of concurrent connections with reasonable RAM. Cross-compilation is straightforward. The standard library handles HTTP, WebSockets, TLS, and JSON without third-party dependencies.
This is essentially the same argument that made TypeScript, Kotlin, Scala, ClojureScript, and Elm all viable: the target runtime is valuable enough that it is worth building a better source language on top of it. You trade the source language’s expressiveness for the target’s ecosystem, tooling, and operational characteristics.
The closest historical precedent for an ML-inspired language targeting Go is Oden, which Oskar Wickström built around 2015-2016. Oden had first-class functions, algebraic data types, and type inference. It stalled partly because Go’s pre-generics type system made polymorphism awkward to compile: every polymorphic function essentially required monomorphization or boxing through interface{}. Go 1.18’s addition of generics in 2022 changed this. A polymorphic function in Sky can now compile to a genuinely generic Go function rather than a pile of generated specializations or runtime type assertions. Sky’s timing, post-generics, is not coincidental.
What Hindley-Milner Gives You Over Go
Go made a deliberate design choice to keep type inference local and annotation requirements high. The language specification is intentionally small. Generic functions require explicit type parameters in most positions. There are no algebraic data types. The idiomatic null representation is nil, which the type system does not track.
Hindley-Milner type inference, developed by Roger Hindley in 1969 and Robin Milner in 1978, takes the opposite position. The algorithm infers the most general type for every expression in the program. You write:
add x y = x + y
And the system infers add : Num a => a -> a -> a without you saying anything about types at all. The algorithm works by assigning fresh type variables to unknowns, walking the AST to generate unification constraints, and solving those constraints via Robinson’s 1965 unification algorithm. The result is always the unique most-general type, or a type error if the constraints are unsatisfiable.
The practical difference for the programmer:
- No annotation burden. You write logic; the types fall out.
- Exhaustive pattern matching over algebraic data types, checked at compile time. The compiler tells you when you have missed a case.
- No null pointer exceptions by construction.
Maybe/Optiontypes make absence explicit and tracked. - Polymorphism that works the way you think it does, without manual type parameter threading.
Go’s generic syntax requires you to write func Map[T, U any](slice []T, f func(T) U) []U. An HM language infers all of that. The verbosity difference compounds across a codebase.
The compilation target gets the safety guarantees of Go’s build system and static binary output. The source language gets ML-family ergonomics. This is the same value proposition Elm made for JavaScript in 2012 when Evan Czaplicki introduced it in his thesis.
The Elm Architecture as a Server Pattern
Elm’s architecture (commonly called TEA, for The Elm Architecture) structures an application as three pure functions:
init : (Model, Cmd Msg)
update : Msg -> Model -> (Model, Cmd Msg)
view : Model -> Html Msg
The runtime holds the model, feeds messages through update, and calls view to produce a new UI description. Side effects are not inline; they are declared as Cmd values and executed by the runtime. Every state change is explicit, auditable, and reproducible.
TEA influenced Redux (Dan Abramov has been explicit about the Elm connection), Vuex, Swift’s The Composable Architecture, the Rust UI frameworks Iced and Seed, and numerous others. The pattern is durable because it makes the relationship between events and state transitions obvious.
What Sky appears to be exploring is running TEA on the server rather than the client. This is not a new idea: Phoenix LiveView, released in 2018-2019, is the most mature example. LiveView keeps component state in a GenServer process on the server, sends minimal diffs to the client over a WebSocket, and handles events through handle_event/3 callbacks that are essentially TEA’s update. The client is a thin JavaScript layer (about 30KB) that applies diffs and dispatches events. LiveView has been running in production at scale for years.
Sky’s bet is that Go’s runtime characteristics make it a compelling alternative to the BEAM (Erlang’s runtime, which powers Phoenix). BEAM is exceptional for fault tolerance and message passing, but Go’s per-goroutine overhead is lower and its binary distribution story is simpler. A Sky application would be a single static binary with no runtime dependency, handling thousands of concurrent UI sessions via goroutines.
Server-Driven UI and What It Actually Means
Server-driven UI (SDUI) is a term that covers several different things, and Sky’s approach seems closest to the LiveView end of the spectrum rather than the Airbnb/Lyft end.
Airbnb’s SDUI system, described in detail in their 2021 engineering posts, sends JSON schemas to native mobile clients that describe UI structure. The client has a registry of components and renders whatever the server describes. This decouples server deployments from client app releases, which matters enormously when App Store review cycles add days of latency to UI updates.
HTMX and Hotwire take a different angle: the server sends HTML fragments; the client swaps them into the DOM. This is closer to traditional server-side rendering but with progressive enhancement for partial updates.
LiveView-style SDUI (and presumably Sky’s approach) is different again: the server holds the full application state and sends wire-protocol diffs when state changes. The client does not interpret a UI schema; it applies a diff to an existing DOM. The round-trip on every user interaction is a real cost, but the advantages are substantial: the client has no application logic, there is no state synchronization problem between client and server, and deploying UI changes requires only a server restart.
For internal tools, administrative interfaces, dashboards, and applications where network latency is predictable, this is a reasonable trade. For consumer applications where every 100ms of perceived latency affects retention, it is harder to justify.
What This Looks Like in Practice
A Sky application in the TEA model would likely look something like this (extrapolated from the Elm architecture and the repository description):
type alias Model =
{ count : Int
, message : String
}
init : Model
init =
{ count = 0, message = "ready" }
type Msg
= Increment
| Decrement
| Reset
update : Msg -> Model -> Model
update msg model =
case msg of
Increment ->
{ model | count = model.count + 1 }
Decrement ->
{ model | count = model.count - 1 }
Reset ->
init
view : Model -> Html Msg
view model =
div []
[ button [ onClick Decrement ] [ text "-" ]
, span [] [ text (String.fromInt model.count) ]
, button [ onClick Increment ] [ text "+" ]
]
The compiler translates this into a Go binary that manages the state machine, serves the initial HTML, maintains WebSocket connections per session, and pushes UI diffs when state changes. The type checker ensures update handles every Msg variant; missing a case is a compile error, not a runtime surprise.
This is where HM inference and TEA reinforce each other. TEA’s architecture makes every state transition explicit and traceable. HM inference ensures the types of every message and model field are correct without annotation ceremony. Together they constrain the space of possible bugs significantly before the program runs.
The Lineage and Where Sky Sits
It helps to see Sky in context:
- Oden (2015-2016): HM-typed ML language targeting Go. The closest direct predecessor. Abandoned before Go had generics.
- Go+: A superset of Go with scripting-friendly syntax. Active and mainstream in China’s education sector. Targets Go’s semantics rather than replacing them.
- Gno: A Go-like language for a deterministic blockchain VM. Interesting for different reasons.
- Phoenix LiveView: Server-side TEA in Elixir. Production-proven. The conceptual proof of concept for Sky’s server-driven approach.
- Iced: TEA for native Rust GUI. Different target (native desktop) but same architectural pattern.
Sky’s specific combination, HM inference plus TEA plus Go output plus server-driven UI, does not have a direct predecessor. The closest thing would be building Phoenix LiveView in a typed functional language that compiled to Go rather than Elixir/BEAM. That is a reasonable thing to want.
The Honest Challenges
Experimental language projects at this stage face predictable obstacles.
HM inference with let-polymorphism is well-understood theoretically, but implementing it correctly and producing good error messages is genuinely hard. Elm’s error messages are famously good, and that quality is the result of years of deliberate engineering work. A new implementation often starts with error messages that tell you types do not unify without telling you why.
Compiling to Go rather than machine code or WASM means the output is only as fast as Go is, and the generated code’s readability affects debuggability. If something goes wrong at runtime, the stack traces point into generated Go, not the original Sky source. Source maps help, but they are additional engineering.
Server-driven UI has latency costs that are real in high-interaction scenarios. The architecture is appropriate for certain applications and wrong for others. The language choice should not paper over that constraint.
And the ecosystem cold-start problem is severe. A new language lives or dies on its tooling: editor support, a package manager, a standard library, documentation, and community. These take years to build.
Why It Is Worth Watching
Despite all of that, the core idea is sound. Go’s runtime is genuinely excellent for the server-driven UI model: low-overhead concurrency, static binary deployment, a solid standard library. HM inference and algebraic data types address real limitations in Go’s type system. The Elm Architecture is a proven pattern for managing UI state. Phoenix LiveView has demonstrated the server-side TEA model at production scale.
Sky is early. The repository does not yet have the surface area of a production framework. But the design space it is exploring sits at the intersection of ideas that have each independently proven their value, and the Go generics addition in 1.18 finally makes the compilation story workable in a way that defeated Oden a decade ago.
For anyone building internal tools or administrative interfaces in Go, a typed functional language with a good server-driven UI story would be genuinely useful. Whether Sky grows into that role depends on execution, community, and the kind of sustained investment that language projects require. The ideas are worth the attention regardless.