Ownership
Move semantics and borrow inference without annotations.
Three Rules
Loon's ownership system keeps memory safe without a garbage collector, and it boils down to three rules. If you internalize these, everything else follows.
- Every value has exactly one owner.
- When a value is assigned or passed, ownership transfers (moves).
- When the owner goes out of scope, the value is dropped.
If you've used Rust, this will feel familiar. The big difference is that Loon never asks you to write lifetime annotations. The compiler figures out borrows on its own, so you get the safety guarantees without the syntactic overhead.
Unlike Rust, you never write lifetime annotations. The compiler infers borrows automatically.
Move Semantics
When you assign a value to a new binding, ownership moves. The original binding becomes invalid, and trying to use it is a compile error. This might feel surprising at first, but it prevents an entire class of bugs where two parts of your program think they own the same data.
LOON
[let a #[1 2 3]]
[let b a]
; a is no longer valid here
[println b]The same thing happens when you pass a value to a function. The function takes ownership, and the caller can no longer use it.
LOON
[fn consume [xs]
[println [len xs]]]
[let items #[10 20 30]]
[consume items]
; items has been movedThis is strict, yes, but it means you always know exactly who is responsible for a piece of data. No shared mutable state, no use-after-free, no data races.
Using a value after it has been moved is a compile-time error.
Auto Borrow Inference
If every function call moved its arguments, you'd spend half your time cloning things. That's where auto borrow inference comes in. When the compiler can prove that a function only reads a value (and the reference won't outlive the owner), it automatically passes a borrow instead of moving.
LOON
[fn total [xs]
[fold xs 0 +]]
[let nums #[1 2 3]]
[println [total nums]]
[println [total nums]] ; works, compiler inferred a borrowYou didn't have to annotate anything. The compiler saw that total only reads xs, so it borrowed instead of moved. That's why you can call total twice on the same value. This is Loon's key ergonomic advantage over Rust: the safety model is the same, but the compiler does more of the bookkeeping for you.
Copy Types
Not everything moves. Primitive types are small and cheap, so Loon copies them instead. When you assign an integer to a new binding, both bindings are valid because each has its own independent copy.
- Int and Float, numeric types
- Bool, true and false
- Char, single characters
- String, strings are immutable and reference-counted
LOON
[let x 42]
[let y x]
[println x] ; still valid, Int is CopyStrings are interesting here. They're reference-counted, so "copying" a string is really just bumping a counter. It's cheap and safe, and it means you can freely pass strings around without worrying about ownership.
Mutation
Loon values are immutable by default. When you need to change something, you opt in explicitly with mut. This makes mutation visible and intentional.
LOON
[let mut items #[1 2 3]]
[set! items [push! items 4]]
[set! items [map items [fn [n] [* n 2]]]]
[println items] ; #[2 4 6 8]The ! suffix on push! and set! is a visual signal that these functions modify their argument. Only the owner of a mutable binding can mutate it, and borrows are always immutable. This means you can't accidentally mutate data through a borrowed reference.
Only the owner of a mutable binding can mutate it. Borrows are always immutable.
Common Errors
The ownership system catches bugs at compile time that would otherwise be subtle runtime issues. Here are the two errors you'll see most often when you're getting started.
Use After Move
This is the classic ownership error. You move a value and then try to use the original binding. The fix is usually to clone the value if you need it in both places, or to restructure your code so only one place needs it.
LOON
[let a #[1 2 3]]
[let b a]
[println a] ; ERROR: value moved to bReturning References to Locals
You can't return a reference to a local variable, because that variable gets dropped when the function ends. The reference would point to nothing.
LOON
[fn bad []
[let x 42]
&x] ; ERROR: x dropped at end of fnThe solution is simple: return the value itself instead of a reference. The compiler is smart about optimizing away unnecessary copies, so returning by value is both safe and efficient.
Return the value itself. The compiler will optimize away unnecessary copies.