Coming from Clojure

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

What's Familiar

Loon is a Lisp, and if you write Clojure, its surface will read like a dialect you already speak. The brackets turned square, but the conviction underneath is yours: small functions transforming immutable data through pipelines.

  • 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 a typical Clojure session means threading data through ->> pipelines and writing small pure functions, the core experience carries over intact. The shape of the work is the same.

What's Different

Square brackets, not parentheses

The most visible change, and the one your muscle memory will resist longest. Loon writes [f x y] where Clojure writes (f x y). Parentheses are reserved for grouping in multi-arity functions. Your fingers retrain in a few days; the mental model never moves.

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

Static types (fully inferred)

This is the deepest philosophical divide. Clojure embraces dynamic typing: values carry their types at runtime, and spec or schema add validation where you choose. Loon has a full static type system, yet it is entirely inferred. You never write a single annotation.

The payoff is that errors Clojure discovers at runtime (wrong argument count, type mismatches, missing keys in a typed map) surface at compile time instead. The cost is that some dynamic patterns you lean on (heterogeneous collections, runtime type dispatch) ask for ADTs rather than mixed shapes sharing one collection.

Ownership instead of persistent data structures

Clojure's persistent data structures are one of its quiet triumphs: structurally shared, immutable collections, reclaimed by a garbage collector. Their elegance comes with GC overhead. Loon reaches for ownership and move semantics instead. Values stay immutable by default, but memory is tracked at compile time rather than swept at runtime.

You trade an occasional thought about when a value is consumed for performance you can predict: no pauses, deterministic cleanup. The compiler shoulders most of the bookkeeping, so the trade is gentler than it sounds.

ADTs instead of maps-as-types

Clojure models domains with maps. A user is {:name "Ada" :age 30} and the map is the contract. Loon keeps maps, but for domain models it leans on algebraic data types:

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

The compiler checks ADTs. You cannot misspell a field, drop one, or add a stray one. Anyone who has spent an afternoon hunting a bug born of a mistyped keyword knows exactly what this buys.

Effects instead of side effects

Clojure is pragmatic about side effects. Any function can perform IO, and purity is a convention you keep rather than a rule the language enforces. Loon tracks effects in the type system through algebraic effects. You still perform IO, but the compiler sees it, and handlers can intercept it. Testing turns simple: swap the handler, leave the code alone.

No runtime metaprogramming

Clojure's dynamism opens the door to runtime eval, protocol extension, and metadata. Loon's metaprogramming happens at compile time, through macros, and stops there. If you lean hard on Clojure's runtime reach, that is a real constraint. It is also the price of admission: a program the compiler can read whole and statically is what gives you type safety with no annotations to write.

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 debugged Clojure in production, a few familiar aches are gone in Loon.

Runtime type errors

No ClassCastException or ArityException in production. No wrong argument count slipping through, no type mismatch found at 2am, no nil surprise cascading down the call stack. The compiler catches them all before the code runs.

Nil punning

Clojure's nil is everywhere, and it travels well. (first nil) returns nil, (get nil :foo) returns nil, and the absence drifts quietly through the program until something breaks, usually far from where it began. Loon has no nil. Absence is named: optional values use Option, and the compiler insists you handle the empty case.

Spec and schema

If you reach for spec or malli to validate data shapes at runtime, Loon's type system does that work at compile time instead. The compiler checks ADTs and typed maps directly. You keep the safety guarantees and shed both the runtime cost and the dependency.

GC tuning

No GC pauses, no heap sizing, no memory pressure ambushing you under load. Ownership makes memory behavior deterministic. A value goes out of scope; it is freed. The whole story fits in one sentence.

New Things to Learn

Most of Loon will feel natural to a Clojure programmer. A few ideas, though, are genuinely new, and they reward the time you give them.

Ownership

Coming from Clojure, this is the genuinely strange one. A value has one owner, and passing it to a function consumes it, unless the compiler auto-borrows, which it does whenever it safely can. That is a different world from persistent data structures, where sharing costs nothing because everything is structurally shared and collected by the runtime.

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

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

Tip

Think of ownership as one reference per value. Clojure lets many references share a value through structural sharing. Loon lets the compiler decide when sharing is safe, and asks you to clone when you genuinely want two independent copies.

Static types

You never write them, yet they are always present. Every expression has a type the compiler knows. The one habit this changes: heterogeneous collections fall out of fashion. Rather than a vector holding both strings and numbers, you name the union with an ADT:

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

It reads as more ceremony than dropping things into a vector, until the day the compiler names the case you forgot to handle.

Effects

Where Clojure lets you call (slurp "file.txt") anywhere, no questions asked, Loon routes IO through the effect system. The call looks much the same, but a handler must be installed to supply the implementation. Testing becomes effortless: install a handler and your IO-heavy code runs against in-memory fakes, no mocking library required.

Pattern matching

Clojure offers case and cond, but no exhaustive matching on data shapes. Loon's match destructures ADTs, and the compiler checks that every variant is handled. Add a new variant to a type and the compiler points you to each place that must change. It is one of those features you stop being able to live without.

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