Coming from JavaScript
What's familiar, what's new, and why it's worth it.
What's Familiar
JavaScript and Loon share more than the distance between them suggests. If you write modern, functional JS (map/filter/reduce, const by default, arrow functions in place of classes), you are already thinking in patterns that carry straight into Loon.
- First-class functions: functions are values, passed around freely.
- Closures: inner functions capture variables from the enclosing scope.
- Map, filter, reduce: the same higher-order patterns you already use.
- Expression-oriented thinking: ternaries, short-circuits, and arrow functions all point toward "everything is an expression." Loon makes that fully consistent.
- Immutable-first style: if you use const by default and avoid mutation, you are already thinking in Loon.
The functional subset of JavaScript (the part without classes, this, prototypes, and mutation) maps almost directly onto Loon. If you have been drifting toward that style for years, Loon is where it arrives.
What's Different
Bracket syntax
The most visible change, and the one that takes the most adjustment. Every call is [function arg1 arg2], not function(arg1, arg2). No commas, no semicolons, no precedence rules to keep in your head.
; JavaScript: Math.max(a + b, c * d)
; Loon:
[max [+ a b] [* c d]]The syntax feels foreign for about a day. Then it clicks, and the uniformity starts to feel like a relief. Everything is a function call. No special syntax for operators, no line between methods and functions, one shape everywhere you look.
Immutable by default
In JavaScript, const freezes the binding, not the value. You can still push to a const array or set fields on a const object. In Loon, a value is genuinely immutable unless you declare it mut. Data structures hold still. There is no array.push() on an immutable vector; you derive a new vector that carries the extra element.
No null or undefined
JavaScript has two kinds of nothing: null and undefined. Loon has none. When a value might be absent, you name that possibility with Option, which is either [Some value] or None. The compiler makes you handle both cases, so Cannot read property of undefined never reaches runtime. That whole family of bugs has nowhere to live.
Ownership
Coming from JavaScript, this is the least familiar idea. In JS you create objects and the garbage collector reclaims them eventually, on its own schedule. In Loon, each value has exactly one owner, and its memory is freed the moment that owner leaves scope. The compiler manages most of this for you, though now and then a "value moved" error appears when you reach for something already consumed.
Note
Ownership errors are caught at compile time, every time. A use-after-free cannot reach runtime. The compiler is strict here so your production code never has to be.
Effects instead of async/await
JavaScript leans on async/await, which introduces the function-color problem: async functions can only be awaited from other async functions, so async-ness seeps through the whole codebase. In Loon, IO, errors, and concurrency all flow through algebraic effects. No coloring. No Promise chains. No await you forgot to write.
Syntax Mapping
JavaScript
Loon
const x = 42
[let x 42]
let arr = [1, 2, 3]
[let arr #[1 2 3]]
const obj = { name: "Ada" }
[let obj {:name "Ada"}]
function add(a, b) { return a + b }
[fn add [a b] [+ a b]]
(x) x * 2
[fn [x] [* x 2]]
arr.map(x x * 2)
[map [fn [x] [* x 2]] arr]
arr.filter(x x > 2)
[filter [fn [x] [> x 2]] arr]
x > 0 ? "pos" : "neg"
[if [> x 0] "pos" "neg"]
`hello ${name}`
[str "hello " name]
console.log(x)
[println x]
x?.foo ?? defaultVal
[match x [Some f] [get f :foo] None default-val]
try { ... } catch (e) { ... }
[handle [...] [Fail.fail e] ...]
What You Can Stop Worrying About
If you have debugged production JavaScript, you have met every one of these. In Loon they are gone.
Null and undefined checks
No if (x !== null && x !== undefined) guards salted through your code. Option and pattern matching make absence explicit and compiler-checked. Forget the None case and the code does not compile.
Type coercion
No "" == false surprises. No [1] + [2] quietly becoming "12". Loon is statically typed and converts nothing on your behalf. What you wrote is what runs.
this binding
No this, no bind, no arrow-versus-regular-function gotchas. A function is a function. It closes over its environment, and nothing more is asked of you.
Async/await complexity
No colored functions. No unhandled promise rejections. No await you meant to write. Effects handle concurrency uniformly, and the compiler will not let an unhandled effect slip past.
Runtime type errors
No TypeError: x is not a function in production at 3am. Every type error is caught at compile time, and you write no annotations to earn it.
New Things to Learn
Loon brings a few concepts with no direct JavaScript equivalent. Each is learnable, and each earns its keep quickly.
Pattern matching
Pattern matching folds if/else chains, switch statements, and optional chaining into one construct. You destructure a value, and the compiler checks that every case is handled. No accidental fallthrough, no branch left behind.
[match result
[Ok value] [println [str "got: " value]]
[Err msg] [println [str "error: " msg]]]Algebraic data types
ADTs do the work you split across objects, classes, and TypeScript union types. They define a closed set of variants, each carrying its own data. Think of an enum whose every case can hold fields of its own.
[type Shape
[Circle Float]
[Rect Float Float]
[Point]]Effects
Effects gather callbacks, promises, async/await, and try/catch into one composable mechanism. Think of them as structured side effects the type system tracks and handlers implement. Code that performs an effect never learns how that effect is served, which is precisely why it is easy to test.
Ownership
Coming from a garbage-collected language, this is the genuinely new idea. A value has one owner and is consumed when passed along. The compiler infers borrows where they are safe, so most of the time it stays out of your way. The curve is real, but far gentler than Rust's, because you never write a lifetime annotation.
Tip
Write code the way you always have. When the compiler says "value moved," clone the value. The intuition for when moves happen builds on its own, and soon you structure code around it without thinking.