Function values
Plan 11 steps 32–35 (2026-10-07): fun F/A, fun M:F/A, anonymous funs
with captured variables (closures), named funs (fun Name(...) -> ... end),
calls of function values (F(Args)) and dynamic calls (M:F(Args),
apply/2,3). Facts come from the pinned maint-29 sources
(erts/emulator/beam/utils.c erts_cmp, erl_printf_term.c, erl_lint)
and probes on OTP 29.1.1.
Values
| Source | Value | Calls enter |
|---|---|---|
fun f/1 | Local fun of this module; every fun f/1 of a module is the same value | f/1 of this module, exported or not |
fun m:f/1 | External fun naming m:f/1, also for the current module | m:f/1 when a module of the program exports it |
fun(X) -> ... end | Local fun capturing the variables it reads from its creator; each evaluation builds a new value | The fun's own generated function |
fun Name(X) -> ... end | The same, with Name bound to the fun inside its clauses | The fun's own generated function |
fun M:F/A with variables | External fun built when evaluated; equal to the literal fun m:f/1 it names | m:f/1 when a module of the program exports it |
fun F/Amust name a function of the module (function F/A undefined) or an auto-imported builtin:fun is_atom/1is the external funerlang:is_atom/1. Funs of builtins call them through the builtin bridge; a builtin outside its catalog (fun self/0,fun erlang:apply/2) reports the unavailabledynamic callscapability.F(Args)evaluatesF, then the arguments left to right, then checks the value: a non-function raises{badfun, F}, another arity{badarity, {F, Args}}(checked before the module), an external fun whose function nothing in the program exportsundef. A call in tail position is a tail call, as for named functions.is_function/1,2are true for funs in bodies and guards.
Dynamic calls
M:F(Args)with a variable (or any expression) as module or function evaluates the module, then the function, then the arguments left to right. A non-atom module or function raisesbadarg; a function no module of the program exports with that arity raisesundef. A call in tail position is a tail call.apply(Fun, Args)andapply(M, F, Args)(auto-imported unless the module definesapply/2,3or suppresses the import; alsoerlang:apply/2,3) evaluate their arguments, then requireArgsto be a proper list (badarg).apply/2then callsFunasF(Args)does ({badfun, Fun},{badarity, {Fun, Args}});apply/3looks upM:F/length(Args)as above, so more than 255 arguments raiseundef. Both are tail calls in tail position and never legal in guards.fun M:F/Awith variables raisesbadargunlessMandFare atoms andAis an integer in 0..255; the function need not exist until the fun is called.- Lookup uses the export tables of the program's modules, which stay
registered for the program's lifetime, so a found function cannot go away
during the call. It scans the modules and their exports, comparing atom
words; hash-map indexes are plan step 62A. A name no module exports is
then looked up among the builtins, so
M:F(...),apply/3and runtimefun M:F/Areacherlangbuiltins of the bridge catalog. - Services.
CLAUSE_call_v1(context, module, function, arity)checks the names and returns theFrameDescriptorof the export (ABI revision 8 adds it toExportDescriptor); the arguments are already in the registers.CLAUSE_apply_list_v1(context, fun, list, registers)andCLAUSE_call_list_v1(context, module, function, list, registers)copy the list into the registers first. All three return null after recording the error, and generated code then transfers through theclause.applymarker as forF(Args).CLAUSE_make_external_fun_v1builds the fun of an externalFunDefinitionthe code server creates once perM:F/A(CodeServer::external_fun).
Closures
- Each clause starts from the scope at the fun. Head variables are new names that shadow outer ones (OTP warns); a name repeated within one head must match. Guards read the head's names. Nothing a fun binds is visible after it, and case-clause names of the enclosing function do not reach into it.
- A fun captures every outer definition its heads, guards or bodies read (nested funs included), in definition order, as OTP orders a function's free variables. Captured values are copied into the fun cell when the fun expression is evaluated, so they survive the creator's return and collections, and copy with the fun.
- The code is a private function
-f/A-fun-N-(N counts the funs off/Ain source order) taking the fun's arguments, then its captured values; stack traces show that name with the combined arity, as OTP's do. No clause matching raisesfunction_clause. - Arguments plus captured values are limited to 255.
- A named fun's clauses see
Nameas the fun itself: a new name shadowing an outer one (OTP warns), never captured and not visible after the fun. A head variable of the same name shadows it in turn. When a clause readsName, the fun's code builds the value on entry from its captured values, so it is equal (=:=) to the fun being called, andName(...)is an ordinary fun call: in tail position it is a tail call and runs in constant stack. - An anonymous fun in a record field default is one fun for every construction that uses the default (OTP expands a copy per site).
Representation
- Descriptor. Each distinct value a module creates compiles to one
abi::v1::FunDescriptorin the module's private<prefix>.funstable (funs.hpp): module descriptor, module and function atom slots, Erlang arity, index, external flag and theFrameDescriptora call enters (null for an external fun outside the program). Registration binds them toFunDefinitions (ModuleAtoms::funs,CodeServer::fun_definition); a local fun's captured value count is its code's arity minus the fun's. - Cell. Boxed kind
fun_closure: header (count1 + n), an untracedconst FunDefinition *, thenncaptured values. Walking, collection, copying and verification skip the definition word, as for native records. - Services.
CLAUSE_make_fun_v1(context, descriptor, captures, count, output)builds a fun.CLAUSE_apply_v1(context, fun, arity, arguments)checks a called value, records the errors above in the failure channel (ErrorReason22-24) and otherwise appends the captured values after the arguments and returns theFrameDescriptorto enter. - Calls. Native form passes the arguments in a word array and calls the
clause.applymarker with the descriptor;lower_framesmakes the array the process registers and the marker a transfer throughCLAUSE_enter_v1or, in tail position,CLAUSE_tail_v1.
Comparison and printing
- Term order: number < atom < fun < tuple < native record < map < nil < list < bitstring (references, ports and pids, which sit between atoms and tuples in OTP, do not exist yet).
- Local funs order before external funs. Local funs compare by module, then
index, then captured values in order (
==compares those with==); external funs by module, function and arity. Equal descriptors give equal values, sofun f/1 =:= fun f/1. erlang:display/1,~wand uncaught-exception reports print an external fun asfun m:f/1(atoms quoted like the emulator does) and a local fun as#Fun<m.Index.0>.
Differences
Recorded in differences:
- A local fun prints
#Fun<m.Index.0>: OTP's index follows its compiler's lambda numbering and its third part is a hash of the module code. Clause numbers local funs in source order, so the order of two local funs of different functions of a module can also differ. - Calling an external fun of a module outside the program raises
undef; OTP would first try to load the module from the code path. The same holds forM:F(Args)andapply/3. - An
undefstack trace starts with the caller's frame; OTP's starts with{M, F, Args, []}for the missing function. - Anonymous fun names (
-f/1-fun-0-, seen in stack traces) count funs in source order; OTP's compiler numbers them in its own order. Funs created inside comprehensions may also capture their values in another order than OTP's, which only shows when two such funs are compared.
Clause