# Functions
Functions are the primary unit of abstraction in Deft. This page covers
the `def` form, parameter syntax, type hints, closures, and the
`@doc`/meta-tag decorators.
## Defining Functions
Use `def` with the body enclosed in braces. Parameters are declared
inside `|...|` at the start of the body. Use `||` for zero params.
def greet {|name^string| @doc "Print a greeting to stdout." echo "Hello, $name!" } def magic-number {|| 42 } greet "world" # => Hello, world! echo [magic-number] # => 42
The body is one or more statements; the value of the last statement is
the return value. There's an implicit `return` — use explicit `return`
only for early exits:
def fib {|n^number| if ($n < 2) { return $n } [$fib ($n - 1)] + [$fib ($n - 2)] } echo [fib 10] # => 55
## Parameter Syntax
Parameters live inside `|...|`. They're comma- or whitespace-separated;
either works.
def add {|a b| ($a + $b) } def add-also {|a, b| ($a + $b) } def variadic {|first @rest| $rest } # variadic via @
### Variadic functions
`@rest` (or any `@name`) collects remaining args into a list. The `@`
mirrors the `@{...}` vector literal — it signals "list-collecting
param". Must be last; the name after `@` is bound verbatim:
def sum-all {|@nums| @doc "Sum any number of arguments." reduce $nums 0 { ($acc + $it) } } echo [sum-all 1 2 3 4] # => 10
### Default values
Parameters may have defaults with `=value`:
def greet {|name="World"| echo "Hello, $name!" } greet # => Hello, World! greet "Alice" # => Hello, Alice!
## Type Hints
Type hints follow the parameter name with a caret (`^`). They serve
three purposes:
1. **Documentation** — communicate expected types to readers.
2. **Validation** — runtime type checking on call entry.
3. **Editor support** — power autocomplete and signature help.
def distance {|p^Point| [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } def greet {|name^string count^number| for i [range $count] { echo "Hello, $name!" } }
Type hints do **not** enforce static typing — Deft remains dynamically
typed. They are "hints" that improve tooling and trigger validation at
specific boundaries (function entry, typed `set`).
### Primitive types
| Hint | Description | Examples |
|---|---|---|
| `string` | Text | `"hi"`, `'world'` |
| `number` | Numeric | `42`, `3.14` |
| `boolean` | Boolean | `true`, `false` |
| `bool` | Alias for boolean | `true`, `false` |
| `nil` | Null | `nil` |
| `list` | List/array | `@{ 1 2 3 }` |
| `map` | Key-value map | `%{ a 1 b 2 }` |
| `set` | Set | `^{ 1 2 3 }` |
| `keyword` | Keyword | `:foo` |
### User-defined types
Hints can reference any `deftype`:
deftype User %{ :name %{:type :string :required true} :email %{:type :string} :age %{:type :number} } def display {|u^User| echo "$u~name <$u~email>" } def alice %User{ :name "Alice" :email "alice@example.com" :age 30 } display $alice
See [Types and Protocols](types-and-protocols) for the full `deftype`
schema reference.
### Runtime validation
Hints trigger validation at call entry:
def greet {|name^string| echo "Hello, $name!" } greet "Alice" # ✓ greet 42 # ✗ Error: type error in 'greet': parameter 'name' expected string, got number
Untyped parameters accept any value, so you can mix freely:
def flexible {|typed^string untyped| # $typed is validated at call time # $untyped can be anything }
### Typed `set`
`set` can carry a type hint that validates the value:
set name^string "Alice" set count^number 42 set user^User [create-user "Bob"]
### Return-type annotations
Can be provided on the function name ...
def fetch-user^string {|id^string| # ... } }
## Named Optional Arguments (`--flag` kwargs)
When a parameter carries a `deftype` hint (any non-primitive `^TypeName`),
Deft treats it as a **flag-collection parameter**. Callers pass `--flag value`
pairs instead of positionally binding that argument; primitive-typed params
in front of it stay positional. This is the idiomatic way to express
optional, named arguments — the parameter type's schema is the contract.
deftype BuildOpts %{ :push %{:type :boolean :default false} :nocache %{:type :boolean :default false} :tag %{:type :string} } def build {|service^string opts^BuildOpts| echo "building $service" if $opts~push { echo " will push" } if $opts~nocache { echo " no cache" } if ($opts~?tag) { echo " tag: $opts~tag" } } build "myapp" --push --tag latest # => building myapp / will push / tag: latest build "myapp" --nocache # => building myapp / no cache build "myapp" # => building myapp (opts omitted entirely)
### How it works
- A parameter whose hint is a user-defined type turns that slot into a
**flag-collection parameter**. It must be the **last** parameter —
everything before it is positional and required; nothing may come
after it.
- At the call site, anything after the positional prefix is parsed as
`--flag value` pairs and assembled into a `%TypeName{...}` instance
using the same rules as the kwarg type constructor (see
[Types and Protocols](types-and-protocols)).
- Boolean fields accept a bare `--flag` shortcut meaning `true`
(`--push` is equivalent to `--push true`).
- Values are type-checked against the schema's `:type`; mismatches raise
at the call site.
- The flag-collection parameter may be **omitted entirely** at the call
site — the value defaults to a fresh instance with all `:default`
values applied (and `nil` for fields without a default).
### Reading flag values
Use `~field` for fields that always have a value (defaulted or required),
and `~?field` when you need to distinguish "not given" from a falsy
default:
if $opts~push { ... } # bool field — false if defaulted if ($opts~?tag) != nil { ... } # was --tag passed at all?
`$m~?field` returns `nil` when the field is absent (null-propagating,
safe to chain); `$m~field` is the plain accessor.
### The flag-collection parameter must be last
Only one flag-collection slot is allowed per function, and it must be
the **trailing** parameter. Internally, the runtime collects the
`--flag value` pairs and appends the resulting `%TypeName{...}` instance
as the final positional argument — so any primitive parameter declared
after it would never receive a value, and the call raises at runtime.
The canonical signature is therefore `positional... opts^Opts`:
def deploy {|app^string env^string opts^DeployOpts| # ... } deploy "billing" "prod" --replicas 5 --dry-run # ✗ invalid — deftype parameter is not last def broken {|opts^Opts app^string| ... }
See [Types and Protocols](types-and-protocols) (› Kwarg construction)
for the parallel feature on type constructors (`[User --name X --dept Y]`).
## Closures
Anonymous functions use the same `|params| body` syntax without `def`:
def square {|x| ($x * $x) } set also-square {|x| ($x * $x) } # assign closure to a variable [map $xs { ($it * 2) }] # implicit $it param [map $xs { |x| ($x * 2) }] # explicit param
### Implicit `$it`
A closure with no parameter list binds the argument to `$it`:
@{ 1 2 3 } |> map { $it * 2 } # $it is 1, then 2, then 3
$xs |> filter { ($it % 2) == 0 }
For multi-arg use, or when you want a clearer name, use explicit params:
@{ 1 2 3 } |> reduce 0 { |acc x| ($acc + $x) }
### Captured locals
Closures capture enclosing locals by reference. Mutations inside the
closure propagate back to the enclosing scope:
def counter {|| set count 0 {|| incr count $count } } set bump $counter echo [$bump] # => 1 echo [$bump] # => 2
This works for `<<`, `set`, and `incr` inside any inner closure
(`each`, `map`, `filter`, `defer`, etc.). The capture is boxed once at
function entry — see [Syntax and Style](syntax-and-style) for the
performance note.
## Multi-Arity Dispatch
A function can have multiple bodies selected by arity. Each arity lives
in its own `{|...| ...}` block:
def range { {|n| [range 0 $n] } {|start end| [range $start $end] } } [range 5] # => @{0 1 2 3 4} [range 2 5] # => @{2 3 4}
This is the canonical pattern for arity-overloaded stdlib functions like
`map`, `filter`, `take`, `drop`, etc. — see [Lazy
Sequences](lazy-sequences) and [Transducers](transducers).
## Decorators (`@`-prefixed metadata)
A function body can carry `@`-prefixed meta tags. They attach metadata
to the function that the runtime, tooling, and your own code can read
back via `meta` / `info/*` (see [Metaprogramming](metaprogramming)).
### Reserved meta names
A small set of decorator names have conventional meaning. Tooling
(signature display, help bars, doc renderers) reads these specifically:
| Decorator | Purpose | Value form |
|---|---|---|
| `@doc` | Function-level documentation string | string (canonical) |
| `@returns` | Return-type annotation (mirrors `def name^RetType`) | string or keyword |
| `@since` | Version when the function was introduced | string |
| `@deprecated` | Mark the function as deprecated | boolean or string |
def distance {|p^Point| @doc "Euclidean distance from origin." @returns :number [math/sqrt (($p~x * $p~x) + ($p~y * $p~y))] } def old-helper {|x| @doc "Use `helper` instead." @deprecated true @since "0.3.0" # ... }
`@returns` and the syntactic `def name^RetType {|...|}` form populate
the same `"returns"` key in metadata — use whichever reads better.
Retrieve at runtime via `meta`:
echo [meta $distance "doc"] # => "Euclidean distance from origin." echo [meta $distance "returns"] # => :number
### Per-parameter docs (`@`)
A decorator whose name matches a declared parameter attaches its value
as that parameter's `:description` in the function's signature metadata
(read by `info/sig`, tooltip renderers, and the TUI help bar). The
param-name match is exact.
def greet {|name^string times^number| @doc "Print a greeting N times." @name "The user's name (required)." @times "How many times to print (defaults to 1)." for i [range $times] { echo "Hello, $name!" } } echo [info/sig "greet"] # => greet(name^string, times^number) | Print a greeting N times.
Only the **string** form populates the param's `:description` slot.
Non-string values under a param-name key still attach to the function's
meta map (under that key), but don't become a param description.
### Custom tags
Any other `@key value` is caller-controlled metadata — use it for
whatever your program or tooling needs. Values may be any literal form
(strings, numbers, booleans, keywords, maps, sets, lists, tagged maps);
the materialiser converts them to the appropriate runtime Value:
def handler {|req| @doc "Handle an HTTP request." @auth-required true @category :experimental @since 3 @tags @{"v1" "stable"} @config %{ :timeout 30 :retries 3 } # ... } echo [meta $handler "auth-required"] # => true echo [meta $handler "since"] # => 3 echo [meta $handler "tags"](0) # => "v1" echo [meta $handler "config"]~timeout # => 30
Repeated keys overwrite — later values win. Function calls and variable
references cannot appear as decorator values (they aren't evaluated at
compile time and are silently dropped); stick to literals.
For the full metadata map on a value, use `info/props`:
echo [info/props $handler] # => full metadata map
### RPC exposure (`@rpc`)
The `@rpc true` tag publishes a function to the runtime's `RpcRegistry`
so other runtimes can call it via `rpc`. Available in every host — see
[RPC](rpc).
def cmd-open-file {|path^string| @rpc true @doc "Open a file in this editor panel." # ... }
### Async functions (`@async`)
The `@async` decorator turns a `def` into an asynchronous function.
Calling it returns a `Future` immediately — the body runs as a
coroutine on the runtime's event loop. The caller resolves the result
with `await`:
def fetch-data {|url^string| @async @doc "Fetch a URL concurrently; resolves to the response body." set resp [http/get $url] return $resp~body } set fut [fetch-data "https://api.example.com/data"] echo $fut # => <future:pending> set body [await $fut] # suspends the current coroutine until done echo $body
`await` on a Future:
- returns immediately if the Future is already resolved,
- suspends the current coroutine (not the whole runtime) if pending,
and resumes when the Future resolves,
- passes non-Future values through unchanged, so it's safe to sprinkle
on values that might or might not be async.
### The body is a coroutine
Inside an `@async` function, you can `await` other Futures and
`yield` control to the event loop. This is what makes I/O concurrency
ergonomic — sequential-looking code that actually runs concurrently:
def aggregate {|urls^list| @async set bodies @{} each { |u| $bodies << [await [fetch-data $u]] # sequential awaits } $urls $bodies }
For wide parallelism (all requests in flight at once), pair `@async`
with explicit task spawning — see [Concurrency](concurrency).
### Calling rules
- Calling an `@async` function **never blocks** — the call returns a
Future synchronously after the coroutine's first suspension point.
- A function with `@async` is itself a regular function value; it can
be passed to `map`, stored in a map, called via `apply`, etc. The
`@async` only affects what calling it *returns*.
- The decorator works on multi-arity bodies as well — each arity
becomes its own async closure.
- Errors inside the body reject the Future rather than propagating
synchronously; `await` re-throws them at the call site.
def risky {|x^number| @async if ($x == 0) { [throw "div by zero"] } ($x / $x) } try { [await [risky 0]] # re-throws here, not at [risky 0] } catch e { echo "caught: $e" }
### Generators (`@generator`)
The `@generator` decorator compiles the function body as a
**coroutine generator** — the same machinery as `defcoro`. Calling the
function does **not** run the body: it returns a `` object
immediately, suspended before the first statement, with the arguments
already bound. Each `yield` in the body hands one value to the caller;
drive the generator with `resume` (or its alias `next`):
def range-from {|start^number| @generator @doc "Yield start, start+1, start+2, ..." set n $start while true { yield $n incr n } } set g [range-from 5] # => <coroutine> — body hasn't run yet [coro-state $g] # => :ready [next $g] # => 5 [next $g] # => 6 [resume $g] # => 7 (resume and next are the same)
Notes:
- Calling a generator function never executes the body — it only
allocates the coroutine and binds the arguments.
- `yield value` suspends the body and returns `value` to the caller; a
bare `yield` returns `nil`.
- When the body runs to completion, the next `resume`/`next` returns
`nil` and `[coro-state $g]` becomes `:done`. `resume` on a finished
generator keeps returning `nil` rather than erroring.
- States are the coroutine states: `:ready` (created, not started),
`:suspended` (between yields), `:done`, plus `:running` and
`:failed`. If the body throws, the error is logged (`CORO ERROR`)
at the resume site.
- All the usual `def` parameter syntax works — type hints, defaults,
rest params — and the generator closure itself is a regular function
value (storable, passable to `apply`, etc.).
- The standalone `defcoro` form is the same thing without the `def`
wrapper; `spawn` + `vwait` drive coroutines on the runtime scheduler
(see [Concurrency](concurrency)).
### Related
- [Concurrency](concurrency) — `deftask`, `defcoro`, `after`, `sleep`,
and the runtime scheduler model.
- `await`, `defcoro`, `yield` — see the in-repl `help` docs for the
per-stdlib function reference.
## Function Values
Functions are first-class values. Pass them as arguments, store them in
data structures, invoke them via `apply` or just by position:
def double {|x| ($x * 2) } [map $xs $double] # pass function value [map $xs double] # bare name also works [apply $double @{5}] # => 10
### `IFn` — callable values
A fixed set of built-in value kinds can be called like a function:
keywords, maps, tagged maps, sets, and vectors:
def lookup %{ :name "Alice" :role :admin } echo [$lookup :name] # => Alice — map called with key def users %{ :alice %{ :role :admin } :bob %{ :role :user } } echo [$users :alice] # => %{ :role :admin } echo [:name $lookup] # => Alice — keyword called on map
This is built-in dispatch, not a user-extensible protocol: `IFn`
callability is fixed to the kinds above, so a `defimpl` on `IFn` for a
user-defined type is never consulted. User-defined type instances are
only callable in the generic tagged-map sense — `[$user :name]` reads
a field. To attach custom behavior to your own type, implement one of
the regular protocols via `defimpl` and call the method explicitly —
see [Types and Protocols](types-and-protocols).
### Partial application with `curry`
`curry` fixes leading arguments, returning a closure that awaits the rest.
When you call the result, the fixed args are prepended to whatever you pass:
def add {|a b| ($a + $b) } set inc [curry $add 1] # fix the first arg echo [$inc 41] # => 42 (calls add 1 41) set ten [curry $add 10] echo [$ten 5] # => 15
`curry` follows the same resolution rules as `apply`, so you can pass a
string/keyword name in place of a function value:
set greet-bob [curry "greet" "Bob"] $greet-bob # calls (greet "Bob")
The returned closure is itself a regular function value — it can be curried
again, passed to `map`/`filter`, or stored in a data structure.
## Recursion and Tail Calls
Tail calls are compiled to in-place loops, so recursive idioms are
safe at any depth:
def count-down {|n^number| echo $n if ($n > 0) { count-down ($n - 1) } } count-down 1000000 # does not overflow
For non-tail recursion that would blow the stack, use an explicit
accumulator or refactor to a loop. Non-tail recursion that isn't refactored
is bounded by `MAX_CALL_DEPTH` (default 4096 frames) — the in-place-loop
optimisation applies only to self-recursive tail calls.
## Closures vs Methods
Deft doesn't have method dispatch on the first argument the way
Clojure does. To attach behaviour to a type, use `defimpl` with a
protocol — see [Types and Protocols](types-and-protocols).
deftype Point %{ :x %{} :y %{} } defimpl Point Drawable { draw {|ctx bounds| [draw/cell $ctx $self~x $self~y "*"] } } defimpl Point Measurable { measure {|| %{ :w 1 :h 1 } } }
`$self` inside an impl body refers to the receiver.