Coming from Clojure

What's familiar, what's different, and where Loon diverges.

What's Familiar

Loon draws heavily from the Lisp family, and if you write Clojure, the surface will feel close to home. The brackets are square instead of round, but the underlying philosophy of "small functions transforming immutable data through pipelines" is essentially the same.

  • S-expression structure: calls are [operator operands], just with square brackets instead of parentheses.
  • Immutable data: values do not change. New values are derived from old ones.
  • Functional style: higher-order functions, closures, and data transformation pipelines are the primary abstractions.
  • Data literals: maps, vectors, and sets have dedicated syntax, not constructor calls.
  • REPL-driven development: Loon has a REPL for interactive exploration.
  • Code is data: Loon's macro system operates on the same data structures the language uses.

If your typical Clojure session is threading data through ->> pipelines and writing small pure functions, you will feel at home almost immediately. The core experience is the same.

What's Different

Square brackets, not parentheses

The most visible change, and honestly the one that will annoy your muscle memory the most. Loon uses [f x y] where Clojure uses (f x y). Parentheses are reserved for grouping in multi-arity functions. Your fingers will need to retrain, but the mental model is identical.

; Clojure: (map inc (filter even? xs))
; Loon:
[map inc [filter even? xs]]

Static types (fully inferred)

This is the biggest philosophical difference. Clojure embraces dynamic typing, where values carry their types at runtime and spec/schema are optional validation layers. Loon has a full static type system, but it is entirely inferred. You never write type annotations.

In practice, this means many errors that Clojure catches at runtime (wrong argument count, type mismatches, missing map keys with typed maps) are caught at compile time in Loon. The tradeoff is that some dynamic patterns you might be used to (heterogeneous collections, runtime type dispatch) require ADTs instead of just throwing different shapes into the same collection.

Ownership instead of persistent data structures

Clojure uses persistent data structures: structurally shared, immutable collections backed by garbage collection. They are brilliant, but they come with GC overhead. Loon uses ownership and move semantics instead. Values are still immutable by default, but memory is managed through compile-time ownership tracking, not a garbage collector.

The result is more predictable performance (no GC pauses, deterministic cleanup) at the cost of occasionally thinking about when values are consumed. The compiler handles most of this automatically, so it is less painful than it sounds.

ADTs instead of maps-as-types

Clojure idiomatically uses maps for domain modeling. A user is {:name "Ada" :age 30} and that is that. Loon supports maps too, but it encourages algebraic data types for domain models:

[type User [User Str Int]]
[let u [User "Ada" 30]]

ADTs are checked at compile time. You cannot misspell a field, forget a field, or add an unexpected one. If you have ever tracked down a bug caused by a typo in a keyword, you will appreciate the tradeoff.

Effects instead of side effects

Clojure is pragmatic about side effects. Functions can do IO freely, and the convention is to keep most functions pure but not enforce it. Loon tracks effects in the type system using algebraic effects. You can still do IO, but the compiler knows about it, and handlers can intercept it. This makes testing much easier: swap the handler, not the code.

No runtime metaprogramming

Clojure's dynamism allows runtime eval, runtime protocol extension, and runtime metadata. Loon's metaprogramming is compile-time only, through macros. This is a real limitation if you rely heavily on Clojure's dynamic capabilities. But it enables the compiler to reason about your entire program statically, which is how you get type safety without annotations.

Syntax Mapping

Clojure
Loon
(def x 42)
[let x 42]
(defn add [a b] (+ a b))
[fn add [a b] [+ a b]]
(fn [x] (* x 2))
[fn [x] [* x 2]]
[1 2 3]
#[1 2 3]
{:name "Ada" :age 30}
{:name "Ada" :age 30}
#{1 2 3}
#{1 2 3}
(if test then else)
[if test then else]
(cond p1 e1 p2 e2 :else e3)
[match val p1 e1 p2 e2 _ e3]
(let [x 1 y 2] (+ x y))
[do [let x 1] [let y 2] [+ x y]]
(->> xs (filter odd?) (map inc))
[pipe xs [filter odd?] [map inc]]
(println "hello")
[println "hello"]
(:name user)
[get user :name]

What You Can Stop Worrying About

If you have spent time debugging Clojure in production, several familiar pain points simply disappear in Loon.

Runtime type errors

No ClassCastException or ArityException in production. No wrong number of arguments slipping through, no type mismatches discovered at 2am, no nil-related surprises cascading through your call stack. The compiler catches them all before your code ever runs.

Nil punning

Clojure's nil is everywhere. (first nil) returns nil, (get nil :foo) returns nil, and nil propagates silently through your program until something finally breaks, often far from the source. Loon has no nil at all. Optional values use Option and the compiler forces you to handle the absent case explicitly.

Spec and schema

If you use spec or malli to validate data shapes at runtime, Loon's type system replaces that need at compile time. ADTs and typed maps are checked by the compiler, not by runtime validation. You get the same safety guarantees without the runtime overhead or the extra library.

GC tuning

No GC pauses, no heap size tuning, no memory pressure surprises under load. Ownership gives you deterministic, predictable memory behavior. When a value goes out of scope, it is freed. That is the whole story.

New Things to Learn

Most of Loon will feel natural to a Clojure programmer. But there are a few concepts that are genuinely new and worth spending time with.

Ownership

This is the most unfamiliar concept coming from Clojure. Values have one owner and are consumed when passed to functions (unless the compiler auto-borrows, which it does whenever it can). This is fundamentally different from Clojure's persistent data structures, where sharing is free because everything is structurally shared and garbage collected.

[let items #[1 2 3]]
[println [len items]]   ; auto-borrowed
[println [len items]]   ; still valid

[let other items]       ; moved
; items no longer valid

Think of ownership as "exactly one reference to each value." Clojure lets many references share a value via structural sharing. Loon lets the compiler decide when sharing is safe, and asks you to clone explicitly when you need two independent copies.

Static types

You never write them, but they are there. Every expression has a type the compiler knows at compile time. The main practical impact is that heterogeneous collections are not idiomatic. Instead of a vector containing strings and numbers, you define an ADT:

[type Value
  [VStr Str]
  [VNum Int]]

This feels more verbose than just throwing things into a vector, but it pays off when the compiler catches a case you forgot to handle.

Effects

Where Clojure lets you call (slurp "file.txt") anywhere without ceremony, Loon tracks IO through the effect system. The code looks similar, but a handler must be installed to provide the IO implementation. This makes testing trivial: swap the handler and your IO-heavy code runs against in-memory fakes, no mocking library needed.

Pattern matching

Clojure has case and cond but no exhaustive pattern matching on data shapes. Loon's match destructures ADTs and the compiler ensures every variant is handled. If you add a new variant to a type, the compiler tells you everywhere you need to update. This is one of those features that is hard to go back from once you have used it.

[match shape
  [Circle r]   [* 3.14159 [* r r]]
  [Rect w h]   [* w h]
  [Point]      0.0]