Coming from JavaScript

What's familiar, what's new, and why it's worth it.

What's Familiar

JavaScript and Loon share more than you might expect. If you write modern, functional-style JS (lots of map/filter/reduce, const everywhere, arrow functions instead of classes), you are already thinking in patterns that transfer directly to 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 to Loon. If you have been gravitating toward that style anyway, this will feel like a natural next step.

What's Different

Bracket syntax

This is the most visible change and the one that takes the most adjustment. Every function call is [function arg1 arg2] instead of function(arg1, arg2). There are no commas, no semicolons, and no operator precedence rules to remember.

; JavaScript: Math.max(a + b, c * d)
; Loon:
[max [+ a b] [* c d]]

The syntax feels unfamiliar for about a day. Then it clicks, and you start to appreciate the uniformity. Everything is a function call. There is no special syntax for operators, no method calls versus function calls, just one consistent pattern everywhere.

Immutable by default

In JavaScript, const prevents reassignment but not mutation. You can still push to a const array or set properties on a const object. In Loon, values are truly immutable unless you explicitly declare them mut. Data structures are immutable. There is no equivalent of array.push() on an immutable vector; instead, you create a new vector with the extra element.

No null or undefined

JavaScript has two kinds of nothing: null and undefined. Loon has zero. When a value might be absent, you use the Option type, which is either [Some value] or None. The compiler forces you to handle both cases explicitly, so you will never see Cannot read property of undefined at runtime. That entire category of bugs simply does not exist.

Ownership

This is probably the most unfamiliar concept if you are coming from JavaScript. In JS, you create objects and the garbage collector eventually frees them, sometime, whenever it feels like it. In Loon, each value has exactly one owner, and memory is freed deterministically when that owner goes out of scope. The compiler handles most of the details automatically, but you will occasionally see "value moved" errors when you try to use something that has already been consumed.

Ownership errors are always caught at compile time. You will never get a use-after-free bug at runtime. The compiler is being strict so your production code does not have to be.

Effects instead of async/await

JavaScript uses async/await for asynchronous operations, which creates a "function color" problem: async functions can only be awaited from other async functions, so the async-ness spreads through your entire codebase. In Loon, IO, errors, and concurrency are all handled through algebraic effects. No function coloring. No Promise chains. No forgetting to await.

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 spent time debugging production JavaScript, you have probably hit every one of these. They are all gone in Loon.

Null and undefined checks

No if (x !== null && x !== undefined) guards scattered through your code. The Option type and pattern matching make absent values explicit and compiler-checked. If you forget to handle the None case, your code does not compile.

Type coercion

No "" == false surprises. No [1] + [2] producing "12". Loon is statically typed with no implicit conversions. What you see is what you get.

this binding

No this, no bind, no arrow-function-vs-regular-function gotchas. Functions are just functions. They close over their environment, and that is it.

Async/await complexity

No colored functions. No unhandled promise rejections. No forgetting to await. Effects handle concurrency uniformly, and the compiler will not let you forget to handle an effect.

Runtime type errors

No TypeError: x is not a function in production at 3am. Every type error is caught at compile time, with no annotations required from you.

New Things to Learn

Loon introduces a few concepts that do not have direct JavaScript equivalents. They are all learnable, and they all pay for themselves quickly.

Pattern matching

Pattern matching replaces if/else chains, switch statements, and optional chaining with a single, powerful construct. You destructure values and the compiler ensures you handle every possible case. No default fallthrough, no forgotten branches.

[match result
  [Ok value]  [println [str "got: " value]]
  [Err msg]   [println [str "error: " msg]]]

Algebraic data types

ADTs replace the combination of objects, classes, and TypeScript union types. They define a closed set of variants, each of which can carry different data. Think of them as enums on steroids, where each variant can hold its own fields.

[type Shape
  [Circle Float]
  [Rect Float Float]
  [Point]]

Effects

Effects replace callbacks, promises, async/await, and try/catch with a single, composable mechanism. Think of them as structured side effects that the type system tracks and handlers implement. The code that performs effects does not need to know how they are handled, which makes it trivially testable.

Ownership

This is the genuinely new concept if you come from garbage-collected languages. Values have one owner and are consumed when passed around. The compiler infers borrows where safe, so most of the time you do not think about it. The learning curve is real but much shorter than Rust's because you never write lifetime annotations.

Start by writing code normally. When the compiler says "value moved," clone the value. Over time, you will develop an intuition for when moves happen and structure your code accordingly.