· 7 min read ·

Sky and the Case for Elm's Architecture on the Server

Source: lobsters

The premise sounds like a programming language Mad Libs: take Elm’s syntax and type system, swap the JavaScript runtime for Go, and use it to emit server-driven UI descriptors instead of HTML. That is Sky, a small experimental language by Andrea Anzellai. It is early-stage, unlikely to be production-ready, and has the star count you would expect for a solo research project. The combination of ideas it is exploring is worth examining seriously regardless, because it brings together three things that have not been designed as a unified language system before: The Elm Architecture as a server request model, server-driven UI as a typed output format, and Go as a compilation target.

The Elm Architecture Off the Browser

The Elm Architecture (TEA) centers on three primitives: a Model that holds all application state as an immutable record, a pure update function that takes a message and a model and returns a new model, and a view function that takes a model and returns a description of what to render. The runtime manages the loop, executing side-effect commands and feeding results back as messages.

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     -> { model | count = 0 }

TEA has been enormously influential. Redux adopted the reducer pattern for React. The Composable Architecture brought it to Swift and iOS. Iced uses it for Rust GUI applications. Gleam’s Lustre applies it for web UIs on the BEAM VM. In each case the insight is the same: pure state transitions are testable, serializable, and amenable to time-travel debugging.

What none of these frameworks fully explore is TEA as the model for a server-side HTTP handler. In Sky’s design, the incoming HTTP request becomes the Msg. The handler loads or initializes a Model. The update function computes the new state. The view function produces the response. The whole pipeline is a pure state machine that happens to live on a server, and Sky makes this the explicit programming model rather than a pattern you apply manually.

The implications compound. Because update is a pure function with no side effects, concurrent request handling becomes trivially safe: each request runs its own state transition with no shared mutable state to coordinate. Because view is a pure function of the model, the response is fully determined by the state. Testing a Sky server is testing pure functions, not spinning up HTTP clients and parsing responses.

Server-Driven UI as a Language Primitive

Server-driven UI (SDUI) has been an infrastructure pattern more than a language-level concern. Airbnb built their Ghost Platform for iOS and Android apps: the server sends JSON trees describing component hierarchies, and native client renderers translate those descriptors into UIKit or Compose components. This allows Airbnb to change UI without an app store release cycle. Lyft, Shopify, and Square have deployed similar systems for parts of their mobile UIs.

A typical SDUI payload looks something like this:

{
  "type": "screen",
  "children": [
    {
      "type": "card",
      "props": {
        "title": "Item",
        "action": { "type": "navigate", "destination": "/item/1" }
      }
    }
  ]
}

The client maintains a registry mapping type strings to native component implementations and walks the tree recursively to render. It is a simple protocol, but the operational complexity lives in keeping server output and client registry in sync as both evolve.

What Sky adds is that the SDUI output format is not bolted onto a general-purpose language. The view function’s return type is the UI descriptor schema, enforced by the type system. This is the same insight Elm had about HTML: instead of string concatenation and runtime failures, make the UI description a typed value. Elm’s Html Msg type catches category errors in your component tree at compile time. Sky’s equivalent applies the same guarantee to server-sent UI descriptors.

In current SDUI implementations written in Go or Java, the component tree is typically assembled from runtime struct marshaling, with no compile-time guarantee of well-formedness. A typo in a component type string surfaces only when a client fails to render. Shifting that class of error to compile time eliminates an entire category of production incidents, the kind that slip through integration tests because the test client tolerates unknown component types gracefully.

Go as the Compilation Target

The choice of Go carries specific consequences worth spelling out.

Single binary output is the most immediate. A statically linked executable with no runtime dependency simplifies container deployments, serverless targets, and distributed systems where startup time and process management matter. Go’s cross-compilation story is excellent: GOOS=linux GOARCH=arm64 go build requires no additional tooling, and the result is a fully self-contained binary. Compare this to a Gleam application on the BEAM, which requires an Erlang runtime; or an Elm application, which requires a JavaScript environment.

Go’s concurrency model is the deeper advantage for a server use case. Goroutines are cheap, channels are expressive, and Go’s scheduler handles IO-bound concurrency efficiently. Sky’s TEA loop maps naturally onto Go’s primitives: each request can run the update and view pipeline in its own goroutine, with no shared state to lock. The functional model and Go’s concurrency model reinforce each other.

Go 1.18’s generics, released in 2022, made Go a substantially better compilation target for ML-family languages. Before generics, compiling algebraic data types to Go required interface{} casts throughout the generated code, which damaged both performance and the readability of generated output. With generics, the generated code can be typed correctly and remains auditable. A Sky-style language in 2018 would have faced a much harder implementation problem.

The impedance mismatch is real and should not be minimized. Go has no union types; Sky’s algebraic data types compile to Go interfaces with type switches, which works but is verbose. Go has no tail-call optimization, so deep recursion in Sky requires trampolining transformations, the same technique Elm applies when targeting JavaScript. Go’s inliner is conservative relative to OCaml or GHC, so heavily functional generated code may not receive the same optimization benefits. These costs are manageable for most server workloads, but they accumulate in performance-sensitive paths.

Hindley-Milner Without the Complexity Ceiling

Sky’s type system follows the Hindley-Milner tradition in Elm’s restricted form rather than Haskell’s extended one. The distinction has practical consequences.

Full HM inference infers the principal type of every expression without annotations. The classic example:

let identity x = x
-- inferred: identity : forall a. a -> a

Let-polymorphism is the key mechanism: identity is polymorphic across all types at each call site, proven at compile time, without requiring a type annotation. This eliminates a broad class of type errors without requiring the programmer to state types explicitly, which is the property that makes ML-family languages pleasant to write.

Elm deliberately removed typeclasses from this foundation. Haskell’s typeclass system extends HM to enable abstractions like Functor, Monad, and Traversable, but at the cost of inference complexity and error message legibility. Elm’s error messages are notably clear in part because the type system does not allow the subtle misuses that typeclasses enable. Sky inherits this design choice.

For server code where maintainability and clear diagnostics matter more than abstraction power, this is a defensible tradeoff. The Go ecosystem itself reflects a similar philosophy: Go resisted generics for over a decade because simpler type rules produce simpler code. Sky’s HM-without-typeclasses sits in the same philosophical territory.

Gleam, the closest comparable language in this space, makes the same choice: no typeclasses, clean HM inference, union types with pattern matching, good error messages. Gleam targets the Erlang BEAM VM and JavaScript rather than Go, giving it different deployment characteristics and a different concurrency model, but the type system philosophy is nearly identical. Gleam is further along in maturity, has a larger community, and has already demonstrated that this design works in production. Sky is exploring whether the same philosophy works with Go’s deployment story substituted for the BEAM’s.

Roc, another language in this family, takes a related approach with “abilities” as a principled alternative to typeclasses, targets native code via LLVM, and uses reference counting rather than GC for more predictable performance. It is still pre-release, but it demonstrates that multiple serious projects are converging on the no-typeclasses-HM design space.

What the Design Requires to Work

The architecture Sky is building is coherent. The combination of TEA as a server model, HM types for correctness guarantees, Go compilation for deployment, and SDUI as a typed output format solves real problems in a principled way. Each piece reinforces the others: immutable model updates are safe to run concurrently in goroutines; typed UI descriptors eliminate a class of client-side rendering failures; single-binary output keeps the operational story simple.

The practical obstacle is cultural. Go developers chose Go in part because they did not want a functional language. The Go community’s sustained resistance to generics, sum types, and higher-order abstractions was not unfamiliarity with those ideas; it reflected a genuine preference for explicit, imperative code. Languages that compile to Go successfully, like CUE for configuration or Starlark for build systems, succeed by targeting a different audience rather than converting Go developers. Sky would need the same clarity about who it is for: developers who want the functional model and find Go’s deployment story compelling.

The SDUI client ecosystem also requires investment that a solo language project cannot bootstrap. Airbnb’s Ghost Platform required years of parallel mobile infrastructure work alongside the server framework. Sky’s server can emit well-typed descriptors, but without established client renderers and component registries, the output has nowhere practical to go for most teams.

Neither of these is an argument that Sky is misconceived. Experimental languages do not need immediate adoption to be valuable. Sky’s contribution is the design itself: evidence that TEA, Hindley-Milner types, Go compilation, and server-driven UI form a coherent system. If that design holds up under continued development, it becomes a reference point for whoever builds this for real, whether in Sky or in a language that learns from it.

Was this interesting?