Functions
<!-- audited: 2026-09-23 -->
How to make, call, compose and inspect functions, and how deep recursion may go.
fn — anonymous functions
fn creates a closure. Brackets delimit the parameter list. Parameters are immutable by default; prefix with @ to allow mutation via assign.
(def double (fn [x] (* x 2)))
(assert (= 42 (double 21)))
# A mutable parameter.
(def bump (fn [@n] (assign n (+ n 1)) n))
(assert (= 11 (bump 10)))
defn — named functions
defn is sugar for (def name (fn [params] body)). It supports a docstring as the first body form.
(defn letter-grade [score]
"Convert a numeric score to a letter grade."
(cond
(>= score 90) "A"
(>= score 80) "B"
(>= score 70) "C"
(>= score 60) "D"
"F"))
(assert (= "A" (letter-grade 95)))
(assert (= "F" (letter-grade 55)))
(assert (= "Convert a numeric score to a letter grade." (doc letter-grade)))
Variadic functions
& collects remaining arguments into a list. &rest is a synonym for & — here and everywhere a rest collector is accepted (destructuring patterns, match patterns, defmacro parameter lists).
(defn sum [& nums]
(fold + 0 nums))
(assert (= 10 (sum 1 2 3 4)))
(defn product [&rest nums]
(fold * 1 nums))
(assert (= 24 (product 1 2 3 4)))
(defn collected [& xs] xs)
(assert (= :list (type-of (collected 1 2))))
Closures
Functions capture their lexical environment. The captured values persist as long as the closure does.
(defn make-counter []
(var n 0)
(fn []
(assign n (+ n 1))
n))
(def counter (make-counter))
(assert (= 1 (counter)))
(assert (= 2 (counter)))
(assert (= 3 (counter)))
Higher-order functions
map and filter keep the collection's type: an array gives an array, a list gives a list.
(assert (= ["A" "B" "C" "F"] (map letter-grade [95 82 71 55])))
(assert (= :array (type-of (map letter-grade [95 82]))))
(assert (= [95 88] (filter (fn [s] (>= s 80)) [95 72 88 61])))
(assert (= 15 (fold + 0 [1 2 3 4 5])))
(assert (= 6 (apply + [1 2 3])) "apply spreads the arguments")
Sorting
sort and sort-with keep the collection's type. sort-by returns an @array for an immutable array (#1241).
(assert (= [1 1 3 4 5] (sort [3 1 4 1 5])))
(assert (= (list "a" "bb" "ccc") (sort-by length (list "bb" "a" "ccc"))))
(assert (= [3 2 1] (sort-with (fn [a b] (compare b a)) [3 1 2])))
Composition and threading
# compose chains functions right to left.
(def shout (compose string/upcase (fn [s] (string s "!"))))
(assert (= "HELLO!" (shout "hello")))
# -> threads as the first argument.
(assert (= 30 (-> 5 (+ 10) (* 2))))
# ->> threads as the last argument.
(assert (= [1 9 25]
(->> [1 2 3 4 5]
(filter odd?)
(map (fn [x] (* x x))))))
fn/signature — the declared shape of a function
(fn/signature f) returns a struct describing how f is called: parameter counts, the rest-collector kind, the &named key set, the inferred signal profile, the docstring, and where the function was written.
(defn greet [name &opt greeting]
"Greet name, with an optional greeting."
(or greeting "Hello"))
(def sig (fn/signature greet))
(assert (= (get sig :name) "greet") "declared name")
(assert (= (get sig :required) 1) "one required parameter")
(assert (= (get sig :optional) 1) "one optional parameter")
(assert (= (get sig :rest) :none) "no rest collector")
(assert (= (get sig :doc) "Greet name, with an optional greeting.")
"docstring")
# The &named key set is part of the signature.
(defn connect [host &named port tls?] [host port tls?])
(def csig (fn/signature connect))
(assert (= (get csig :rest) :named) "&named collector")
(assert (= (get csig :named-keys) [:port :tls?]) "sorted key set")
# The signal profile has the shape compile/signal returns.
(def id-sig (fn/signature (fn [x] x)))
(assert (get (get id-sig :signals) :silent) "identity is silent")
:rest is :none, :list (&), :keys (&keys), or :named (&named). :named-keys is a sorted array of keywords, empty unless :rest is :named. :name, :doc, and :origin (a {:file :line :col} struct) are absent when the function does not carry them. A native primitive answers from its declared metadata. Any other value is a type-error.
Tail call optimization
Tail calls run in constant stack space.
(defn sum-to [n acc]
(if (= n 0)
acc
(sum-to (- n 1) (+ acc n))))
(assert (= 5000050000 (sum-to 100000 0)) "no stack overflow")
Recursion depth
A non-tail call waits in the fiber, not on the thread's native stack. A recursion can therefore go as deep as memory allows, up to the depth cap.
(defn count-down [n]
(if (= n 0) 0 (+ n (count-down (- n 1)))))
(assert (= (count-down 100000) 5000050000) "non-tail recursion 100,000 deep")
The cap is 10,000,000 calls in progress on one fiber, and (vm/config-set :max-depth n) changes it (config.md). A call past the cap halts the program with :stack-overflow. The halt is not an error, so protect does not catch it.
A recursion that passes through a primitive still uses the native stack at each level: a trait method that calls the primitive it implements, or eval of a form that calls eval. When that stack runs low, the program halts with :stack-overflow too.
See also
- named-args.md — &named, &keys, &opt, default
- destructuring.md — unpacking in function params
- signals — how signals affect function contracts
- control.md — conditionals and loops