Functions
Named, anonymous, higher-order, and recursive.
Named Functions
Functions are defined with fn. You give it a name, a parameter list in brackets, and a body. The body has an implicit do, so you can write multiple expressions and the function returns the last one. No return keyword needed.
[fn greet [name]
[println "Hello, {name}!"]]This defines a function called greet that takes one argument. Because fn bodies get that implicit do, you could add more expressions before the println and they'd all execute in order.
Anonymous Functions
Drop the name and you get a lambda. These are first-class values, which means you can bind them to variables, pass them to other functions, or return them. You'll use these constantly with map, filter, and friends.
[let double [fn [x] [* x 2]]]
[println [double 5]] ; 10The syntax is identical to a named function, just without the name in between fn and the parameter list. Loon doesn't have a separate "lambda" or "arrow function" syntax. It's all just fn.
Multi-Arity
Sometimes you want a function that behaves differently depending on how many arguments it receives. Loon handles this with multi-arity definitions, where each arity is wrapped in parentheses inside the same fn form. The runtime dispatches to the right one based on argument count.
[fn greet
([name] "Hello, {name}!")
([first last] "Hello, {first} {last}!")]Call [greet "Ada"] and you get the single-argument version. Call [greet "Ada" "Lovelace"] and you get the two-argument version. This is cleaner than optional parameters or overloading because each arity has its own explicit parameter list and body.
Variadic Functions
Use & before the last parameter to collect any remaining arguments into a vector. This is useful for functions that accept a flexible number of inputs, like a logging function that takes a severity level followed by any number of message fragments.
[fn log [level & msgs]
[println [str level ": "
[join msgs " "]]]]Here, level captures the first argument and msgs captures everything else as a vector. So [log "WARN" "disk" "almost" "full"] would print WARN: disk almost full.
Higher-Order Functions
A higher-order function is one that takes or returns another function. This is where functional programming really shines. Instead of writing loops, you describe transformations. The standard library gives you the classics: map to transform each element, filter to keep the ones you want, and fold to reduce a collection down to a single value.
[map [fn [x] [* x x]] #[1 2 3 4]]
; #[1 4 9 16]
[filter [fn [x] [> x 2]] #[1 2 3 4]]
; #[3 4]If you find yourself writing a for loop that builds up a new list by transforming each element, that's map. If you're writing a loop that skips some elements, that's filter. Learning to see these patterns will change how you think about data processing.
Closures
Functions capture variables from their enclosing scope. This means you can create functions that "remember" values from when they were defined. The classic example is a function factory:
[fn make-adder [n]
[fn [x] [+ x n]]]
[let add5 [make-adder 5]]
[println [add5 10]] ; 15make-adder returns a new function that closes over n. Each call to make-adder creates a fresh closure with its own captured value. add5 will always add 5, no matter where or when you call it.
Pipe
When you chain several transformations together, nested function calls get hard to read. pipe solves this by letting you write transformations top-to-bottom instead of inside-out. The result of each step is passed as the last argument to the next.
[pipe #[1 2 3 4 5]
[filter [fn [x] [> x 2]]]
[map [fn [x] [* x 10]]]
[each println]]Read this as: start with the vector, filter to keep values greater than 2, multiply each by 10, then print each one. Compare that to the nested version: [each println [map [fn [x] [* x 10]] [filter [fn [x] [> x 2]] #[1 2 3 4 5]]]]. The pipe version reads like a recipe. The nested version reads like an onion.
pipe eliminates deeply nested calls and temporary variables.
Recursion
Loon supports recursion, and pattern matching makes recursive functions particularly clean. You define your base case and recursive case as match arms, and the structure of the code mirrors the structure of the problem.
[fn factorial [n]
[match n
0 1
_ [* n [factorial [- n 1]]]]]The _ pattern matches anything, so this reads naturally: factorial of 0 is 1, and factorial of anything else is n times factorial of n minus 1. We'll cover pattern matching in more depth in the Pattern Matching guide.