Coming from Rust

What's the same, what's different, and what you can stop worrying about.

What's Similar

If you know Rust, you already hold the philosophy. Loon rests on the same convictions: ownership beats garbage collection, types should be algebraic, null was a mistake, and the compiler should catch all it can. These ideas cross over without translation:

  • Ownership and move semantics: values have one owner, moves are the default.
  • Pattern matching: exhaustive, first-class, with destructuring.
  • Algebraic data types: enums with data, used instead of class hierarchies.
  • No null: Option replaces null, Result replaces exceptions.
  • Immutable by default: mutation is explicit and controlled.
  • Zero-cost abstractions: high-level code compiles to efficient output.

If these ideas are already second nature, Loon will feel familiar at the conceptual level. The differences come down to one question: how much does the compiler work out for you, and how much must you spell out?

What's Different

No lifetime annotations

This is the headline. Loon's compiler infers every borrow and lifetime for you. You never write 'a, &, or &mut. Take the classic "longest string" example:

; Rust:
; fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

; Loon:
[fn longest [x y]
  [if [> [len x] [len y]] x y]]

The compiler works out the borrowing; you write the logic. Nothing is given up for the convenience. The same ownership rules hold, inferred rather than spelled out.

Bracket syntax

Loon trades C-style curly braces for Lisp-style brackets. Every call is [function arg1 arg2]. No operators with special syntax, no precedence to memorize, no ambiguity about where a function call ends and a keyword begins.

Note

This is a real tradeoff. Bracket syntax is more uniform and far easier to parse, for compilers and for macros alike, but it reads as unfamiliar at first. Most Rust programmers settle in within a day or two.

Hindley-Milner inference

Rust's inference is local: it resolves types inside a function but asks for annotations at the boundaries. Loon's inference is global, Hindley-Milner. You annotate a signature only when you want to, for the reader's sake. Everything else, generic type parameters included, the compiler works out.

Effects instead of traits for IO

Where Rust reaches for traits like Read, Write, and Future, Loon reaches for algebraic effects. They serve the same end, abstracting over behavior, but with resumption and handler composition built in. Think of dependency injection the type system understands, with more structure.

Syntax Mapping

Rust
Loon
let x = 42;
[let x 42]
let mut v = vec![1,2,3];
[let mut v #[1 2 3]]
fn add(a: i32, b: i32) -> i32 { a + b }
[fn add [a b] [+ a b]]
if x > 0 { "pos" } else { "neg" }
[if [> x 0] "pos" "neg"]
match opt { Some(x) x, None 0 }
[match opt [Some x] x None 0]
v.iter().map(|x| x * 2).collect()
[map [fn [x] [* x 2]] v]
x.foo().bar().baz()
[pipe x [foo] [bar] [baz]]
println!("hello {}", name);
[println [str "hello " name]]
struct / enum
type / ADT
impl Trait for Type
[effect Name ...]

What You Can Stop Worrying About

If you have gone a few rounds with the Rust compiler, this section reads like setting down a heavy pack. Here is what Loon carries for you.

Lifetimes

No 'a, no 'static, no "does not live long enough" trailed by a cryptic hint about adding lifetime bounds. The compiler infers all of it. You will never read a lifetime annotation in Loon, because the syntax for one does not exist.

Trait bounds

No where T: Clone + Send + Sync + 'static cascades. Loon's inference and effect system retire most trait bounds. Write a generic function and the compiler works out what capabilities the type parameter needs.

Turbofish

No :: anywhere. The compiler always infers type parameters from context. Anyone who has untangled a turbofish knows the quiet relief of its absence.

Fighting the borrow checker

Loon's ownership model is built so the compiler settles borrowing questions silently. You will not rearrange sound code to appease a borrow checker. If the logic is correct, it should compile.

Async coloring

No async/.await coloring problem. Concurrency is an effect, handled at the boundary. Functions never declare themselves async, and you never maintain parallel sync and async versions of an API.

New Things to Learn

Loon is not "Rust with less syntax." A few concepts here are genuinely new, and worth the time it takes to make them yours.

Effects

Algebraic effects take over the work traits do for IO, error handling, and state. The mental model: declare the operations you need, perform them wherever you like, and let a handler at the boundary choose the implementation. That pays off twice over, in testing (swap the handler, not the code) and in composition (effects combine without the monad-transformer headaches you may recall from Haskell).

Tip

Picture effects as dependency injection the type system tracks, and your intuition will point the right way.

Bracket syntax

Every call is [f x y]. There is no operator precedence, so [+ 1 [* 2 3]] is unambiguous on its face. It feels odd for about a day, then turns invisible, and in time you come to value having a single syntax for "call this thing with these arguments."

Pipe

Where Rust chains methods, Loon uses pipe. Each step's result becomes the last argument to the next function. It reads naturally once it is in your hands, and it works with any function, not only methods bound to a type.

[pipe data
  [filter even?]
  [map [fn [x] [* x 2]]]
  [take 10]]