How to work with functions¶
This page explains how to define, call, and manage functions in q, including arguments, scoping, projections, recursion, variadic arguments, and return behavior.
A function in q is a reusable block of logic: a sequence of expressions that execute in order. Functions can take zero or more inputs and return an output.
Defining a function¶
Define a function in q using lambda notation, which consists of:
- Curly braces
{}for the function body - An optional argument list (up to 8 named parameters) inside square brackets
[] - One or more expressions separated by semicolons
;
For example, the following code shows a lambda named plus that adds two arguments:
q
q)plus: {[x; y] x + y}
q)plus[22; 33]
55
Python
>>> def plus(x, y):
... return x + y
...
>>> plus(22, 33)
55
In q, a lambda refers to any function defined using this notation, whether anonymous or assigned to a name.
Functions as data¶
Functions in q behave like other data types (integers, symbols, and so on). Use type to confirm a function's type:
q)func: {[x; y] x+y}
q)type func
100h
A return value of 100h indicates a lambda. You can:
- Pass functions between processes via IPC
- Use them as arguments or return values in other functions
- Store them in lists or dictionaries
Functions that operate on or return other functions are called higher-order functions.
Function rank¶
The rank (or valence) of a function is the number of arguments it accepts. Functions are often described by their rank:
- Niladic: accepts no arguments (rank 0)
- Monadic/Unary: accepts one argument (rank 1)
- Dyadic/Binary: accepts two arguments (rank 2)
You may define functions with up to 8 arguments. Using more raises a params error:
q)f: {[a; b; c; d; e; f; g; h] a*b-c+d*e-f*g+h}
q)f: {[a; b; c; d; e; f; g; h; i] a*b-c+d*e-f*g+h+i}
'params
Pattern matching offers a simple way around the limit. Group parameters that belong together, pass them as one list, and unpack that list in the signature: the group counts as a single parameter, so the function stays within rank 8.
q)f: {[a; b; c; d; e; f; (g; h; i; j)] a*b-c+d*e-f*g+h*i-j}
q)f[1; 2; 3; 4; 5; 6; (7; 8; 9; 10)]
-45
q)f[1; 2; 3; 4; 5; 6; 7 8 9 10] / a simple list works too
-45
The list pattern also checks the size of the group, so a caller who passes the wrong number of items gets an error rather than a silent null:
q)f[1; 2; 3; 4; 5; 6; (7; 8; 9)]
'length
[1] f:{[a; b; c; d; e; f; (g; h; i; j)] a*b-c+d*e-f*g+h*i-j}
^
Rank itself is still capped at 8
Grouping keeps a function within the cap rather than lifting it. enlist is the only built-in that takes more parameters, and the only one whose rank is not fixed. See variadic functions for how to build on that to accept a variable number of arguments.
Functions can inspect the type or length of their arguments at runtime using type or count, and branch accordingly.
Calling a function¶
Call a function in q using function application syntax: the function name followed by arguments inside square brackets [], separated by semicolons ;.
q)func: {[x; y] x+y}
q)func[22; 33]
55
If the function doesn't require arguments, include empty brackets:
q)func: {show "func called";}
q)func[]
"func called"
Functions that take only one argument also support prefix notation, omitting brackets:
q)f: {[x] x*x}
q)f[22]
484
q)f 22 / prefix notation
484
q evaluates expressions inside argument positions before passing them:
q)func: {[x; y] x*y}
q)func[2+8; 2]
20
Calling a function with too many arguments results in a rank error:
q)func: {[x; y] x*y}
q)func[22; 33; 231; 123]
'rank
Calling a function with fewer arguments than declared results in a projection.
You can call a function directly using its variable name or indirectly by symbol:
q)func: {[x; y] x+y}
q)func[10; 2]
12
q)`func[10; 2]
12
q)a: `func
q)a[10; 2]
12
Anonymous functions¶
Anonymous functions are useful for one-off operations or dynamic evaluation. Define and call one inline:
q){[x] x*x} 2
4
Use an anonymous function with each to apply logic to every item in a list. In the example below, a list of server addresses becomes a list of connection handles. The anonymous function first turns each string into a symbolic handle using hsym, then opens a connection using hopen:
q)machines: ("qa-machine1:1234"; "qa-machine2:5234"; "qa-machine3:2234")
q)h: {[x] hopen hsym `$x} each machines
q)h
6 7 8i
You can also store anonymous functions in lists, dictionaries, or tables to support dynamic dispatch: index the collection to pick a function at runtime, then apply the result.
q)power: ({1}; {x}; {x*x}; {x*x*x})
q)selected: 2
q)power[selected][5]
25
Function arguments¶
Implicit arguments¶
Define a function without naming its arguments and q provides up to three implicit arguments, x, y, and z, in that order:
q)f: {22+x+x} / implicit single argument x
q)f[2]
26
q)f: {y-x} / implicit arguments x and y
q)f[2; 10]
8
q)f: {z*z} / implicit argument z
q)f[2; 3; 4]
16
Anonymous functions benefit from implicit arguments too:
q)h: {hopen hsym `$x} each ("qa-machine1:1234"; "qa-machine2:5234") / implicit single argument x
A function with named arguments is called signed. Without named arguments, it is unsigned.
Argument type checking¶
Declare argument types inside the function signature using a single-character type code after a colon (:). For example, use `j to restrict a parameter to type long:
q)f: {[someLong:`j] 2*someLong}
q)f[2]
4
q)f[2.2]
'type
[1] f:{[someLong:`j] 2*someLong}
^
Type-checked and plain parameters mix freely in one signature, so you can constrain only what matters:
q)showInfo: {[name:`s; weight] string[name], " is ", string weight}
q)showInfo[`devi; 10]
"devi is 10"
q)showInfo[`devi; 10.5]
"devi is 10.5"
This enforces type safety and improves code robustness. Type checks are one kind of pattern; the same mechanism also unpacks lists and dictionaries.
Filter functions¶
A type check rejects a value; a filter function transforms it. The pattern name:expr runs the callable expr on the argument and assigns what it returns, so the body of the function only ever sees one canonical form.
The simplest filter is a cast, which coerces the argument where a type check would refuse it. The lookup below is keyed by date, and the cast lets callers pass a timestamp just as well:
q)closes: 2026.02.02 2026.02.03!214.26 215.1
q)closeOn: {[d:`date$] closes d}
q)closeOn 2026.02.03
215.1
q)closeOn 2026.02.03D09:30:00.000 / narrowed to its date
215.1
The cast is doing real work here: a timestamp cannot index a date-keyed dictionary on its own.
q)closes 2026.02.03D09:30:00.000
'type
[0] closes 2026.02.03D09:30:00.000
^
Two idioms earn their keep. The first guarantees a list, so a body that indexes or iterates cannot be handed an atom. Without it, x[0] on an atom reads as an attempt to use x as a file handle:
q){x[0]} 42
'Cannot write to handle 42. OS reports: Bad file descriptor
[1] {x[0]} 42
^
q){[x:(),] x[0]} 42 / (), makes a one-item list of an atom, and leaves a list alone
42
The second normalizes an argument that arrives in several shapes. Here the caller may pass a database root as a string, a symbol, or a file symbol, while the body works with a file symbol throughout:
q)getFSym: {hsym $[10h ~ type x; `$; ] x}
q)fn: {[db:getFSym] db}
q)fn "/tmp/kdbdb"
`:/tmp/kdbdb
q)fn `:/tmp/kdbdb
`:/tmp/kdbdb
A filter function signals errors like any other function, which makes it a reusable validator too. Give it a second parameter and apply it as a projection, and a single definition can report which parameter failed:
Multiline definitions belong in a script
A q expression may span several lines only in a script, and its continuation lines must be indented and contiguous — see multiline expressions. Definitions that span lines are therefore shown as script code (no q) prompt), and the calls as session input.
positiveNumber: {[name:`C; v]
if[not 0 < v; 'name, " must be a positive number"];
if[not .Q.ty[v] in "HIJ"; 'name, " must be a short, int or long"];
v
}
compound: {[n: positiveNumber["compounding frequency"]] 1 % n}
q)compound 12
0.08333333
q)compound -1
'compounding frequency must be a positive number
[2] positiveNumber:{[name:`C; v]
if[not 0 < v; 'name, " must be a positive number"];
^
if[not .Q.ty[v] in "HIJ"; 'name, " must be a short, int or long"];
[1] compound:{[n: positiveNumber["compounding frequency"]] 1 % n}
^
q)compound 12.
'compounding frequency must be a short, int or long
[2] positiveNumber:
if[not 0 < v; 'name, " must be a positive number"];
if[not .Q.ty[v] in "HIJ"; 'name, " must be a short, int or long"];
^
v
[1] compound:{[n: positiveNumber["compounding frequency"]] 1 % n}
^
.Q.ty reports an atom in uppercase
.Q.ty returns an uppercase type character for an atom and a lowercase one for a list — the opposite of the type-check pattern, where `j matches a long atom and `J a long list.
Filter functions apply to any assignment, not only to a signature, so a function can also validate a value that it computes or unpacks itself:
q)setFreq: {(n: positiveNumber["compounding frequency"]): x; 1 % n}
q)setFreq 4
0.25
Keeping validation in filter functions separates it from the core logic, which leaves both easier to test and to document.
Named arguments¶
q applies arguments by position, but a dictionary pattern in the signature has the effect of named arguments: the caller passes a single dictionary, and the pattern unpacks the entries it names into local variables. The shorthand ([name; weight]) names both the keys to read and the variables to bind.
q)showInfo: {[([name; weight])] string[name], " is ", string weight}
q)showInfo ([name: `devi; weight: 10])
"devi is 10"
Keys are matched by name, so order is irrelevant and any entry the signature does not mention is ignored. A key the signature requires and the caller omits raises a match error:
q)showInfo ([weight: 10; name: `devi]) / order does not matter
"devi is 10"
q)showInfo ([weight: 10; name: `devi; extra: 42]) / extra entries are ignored
"devi is 10"
q)showInfo ([n: `devi; weight: 10]) / no 'name' entry
'match
[0] showInfo ([n: `devi; weight: 10])
^
Each value in a dictionary pattern is itself a pattern, so type checks still apply. Write the value as name:`s to bind and check in one step:
q)showInfo: {[([name: name:`s; weight: weight:`j])] string[name], " is ", string weight}
q)showInfo ([name: `devi; weight: 10])
"devi is 10"
q)showInfo ([name: `devi; weight: 10.5])
'type
[1] showInfo:{[([name: name:`s; weight: weight:`j])] string[name], " is ", string weight}
^
([name:`s]) is a constant pattern, not a type check
A bare type character in the value position is matched as a value, so `s there means the symbol `s itself, and nothing is bound to name:
q)g: {[([name:`s])] name}
q)g ([name: `devi])
'match
[1] g:{[([name:`s])] name}
^
Supply defaults with a filter function: project join with a dictionary of defaults on its left, and any entry the caller passes prevails over them.
q)showInfo: {[([name; weight]): ([weight: 1]),] string[name], " is ", string weight}
q)showInfo ([name: `agnes])
"agnes is 1"
q)showInfo ([name: `agnes; weight: 70])
"agnes is 70"
This is the readable option for a handful of parameters. For a function with one mandatory parameter and many optional ones, keep the mandatory parameter positional and pass the rest as a dictionary.
Local and global variables¶
Use local and global variables to control scope and data visibility in q functions. Variable assignment and access behave differently depending on the context.
Local variables¶
Create local variables inside a function using :. Local variables:
- Exist only during function execution
- Remain invisible outside the function scope
- Cannot be used in call-by-name functions
- Stay hidden from local functions nested inside the same scope
Local assignments don't affect external values:
q)v: 22
q)f: {[x] x: 100}
q)f[v]
100
q)v
22
Modifying elements of a passed-in vector also has no lasting effect:
q)v: 1 2 3
q)f: {[x] x[0]: 100}
q)f[v]
q)v
1 2 3
Assignments with : apply locally and don't overwrite global variables:
q)a: 22 / global 'a'
q)f: {[x] a: 33; b: 44; a+b+x} / function creates local 'a' and 'b', local 'a' takes precedence
q)f[1]
78
q)a / global 'a' remains unchanged
22
q)b / no global 'b' defined, error occurs
'b
A name that is not assigned locally is read from the current namespace, never from the locals of the caller:
q)a: 42 / assigned in root
q)f: {a+x}
q)f 1 / f reads a in root
43
q){a: 1000; f x}1 / f still reads a in root, not the caller's local a
43
Avoid assigning locals conditionally
Local variables initialize to the empty list (), so a branch that skips the assignment leaves the variable holding () rather than a usable value:
q
q)t: ([]0 1)
q){select from t}[] / global t
x
-
0
1
q){if[x; t: ([]`a`b)]; select from t} 1b / local t
x
-
a
b
q){if[x; t: ([]`a`b)]; select from t} 0b / local t is ()
'type
[1] {if[x; t: ([]`a`b)]; select from t}
^
Global variables¶
Names not defined locally resolve from the current namespace:
q)v: 10
q)f: {[x] x+v}
q)f[2]
12
Resolution depends on the active namespace:
q)v: 10
q)\d .foo
.foo){[x] x+v}[2] / errors as .foo.v doesn't exist
'v
.foo)v: 20
.foo){[x] x+v}[2] / resolves to .foo.v
22
.foo)\d .
q){[x] x+v}[2] / resolves to v in the root namespace
12
Assign global values using ::. These assignments persist beyond the function call:
q)v: 0
q)f: {[x] v:: x; x+x}
q)f[1]
2
q)v
1
q)f[2]
4
q)v
2
:: binds to a local of the same name if one exists
q)v: 0
q)f: {[x] v: 0; v:: x; x+x}
q)f[1]
2
q)v
0
Use set to reliably write to global variables:
q)v: 0
q)f: {[x] v: 0; `v set x; x+x}
q)f[1]
2
q)v
1
Use get to access global variables by name:
q)x: 22
q)f: {[x] show x;} / prints local variable x
q)f 101
101
q)f: {[x] show get `x;} / prints global variable x
q)f 101
22
Passing variables by reference¶
By default, q passes arguments by value. To avoid copying large vectors or tables, pass the variable name as a symbol instead and read or write it with get and set:
q)a: 10 20 30 40 50
q)f: {[x] show get x; x set 10 20 30;}
q)f[`a] / call function, passing variable name
10 20 30 40 50
q)a / function used set to change variable
10 20 30
Return values and exit behavior¶
Control what your function returns, whether it's a result, an early exit, or no value at all.
If the final expression does not end with a semicolon, the function returns its value:
q)f: {[x] x+x}
q)f[2]
4
To return early, use an empty assignment (: followed by a value):
q)c: 0
q)f: {a: 6; b: 7; :a*b; c:: 98}
q)f 0
42
q)c
0
End the last statement with a semicolon to suppress the return value. The function returns the generic null:
q)f: {2*x;} / last statement is empty
q)f 10 / no result shown
q)(::)~f 10 / matches generic null
1b
A stray trailing semicolon silently discards the result
The semicolons in a body are separators, not terminators. One left behind while editing turns a working function into a void one, which then returns the generic null wherever its result was used — with no error at the point of the mistake:
q)f: {2*x}
q)1 + f 10
21
q)f: {2*x;} / semicolon added by accident
q)1 + f 10
'type
[0] 1 + f 10
^
Signal an error or abort¶
To abort evaluation immediately, use signal, which is ' with a value to its right:
q)c: 0
q)g: {a: 6; b: 7; '`TheEnd; c:: 98}
q)g 0
'TheEnd
[0] g 0
^
Early return and error signaling combine naturally for input validation:
q)f: {if[7h<>type x; '"type"]count x}
q)f[33 22]
2
q)f[22.4]
'type
[0] f[22.4]
^
Recursive functions¶
.z.s refers to the function currently being executed, so a function recurses without hardcoding its own name:
q)fact: {$[x <= 1; 1; x * .z.s x-1]}
q)fact 5
120
Calling the name instead — {$[x <= 1; 1; x * fact x-1]} — works too, but the body then depends on that global, so a copy under another name breaks. An anonymous function has no name at all, leaving .z.s as the only option.
.z.s is the innermost function, not the one you are writing
Inside a nested lambda, .z.s refers to that lambda:
q){[x] ({.z.s} x)} 1
{.z.s}
Iterators such as each introduce no function, so .z.s each x recurses on the enclosing function as intended.
Recursion stops at about 2000 nested calls with a stack error, and mutual recursion still needs names, since .z.s cannot reach outside the running function. The accumulators have neither limit and are usually the better tool — Converge applies a unary function until the result stops changing, and its While form stops on a condition. Use .z.s only where an iterator cannot do the job.
q)(raze/) (1; (2; (3; 4))) / flatten to any depth, no recursion
1 2 3 4
q)count {x+1}\[{x < 5000}; 0] / 5000 iterations, no stack error
5001
Projections and partial application¶
When you call a function with fewer arguments than its defined rank, q returns a projection: a partially applied function. The projection "locks in" the arguments you supplied and waits for the rest. Other languages may refer to this concept as currying.
q
q)add: {[x; y] x+y}
q)add42: add[42;] / fix the first parameter to 42
q)add42[2]
44
q)add42[4]
46
Python
>>> from functools import partial
>>> add = lambda x, y: x + y
>>> add42 = partial(add, 42)
>>> add42(2)
44
>>> add42(4)
46
A projection displays as the original function with the arguments you fixed, so the console shows what it is still waiting for:
q)add[42;]
{[x; y] x+y}[42;]
Juxtaposition projects on the whole argument
Prefix notation passes one argument, so applying a binary function that way projects rather than calls it — and a list on the right becomes a single fixed argument, not two:
q){x+y} 42 / a projection, not 42+y
{x+y}[42]
q){x+y} 42 3 / the list is one argument
{x+y}[42 3]
q){x+y}[42;] 3 / brackets to fix the first parameter
45
Chain projections by supplying arguments over multiple stages:
q)addm: {[x; y; z] x+y+z}
q)addm[2;; 3][6] / fix 1st and 3rd parameter, then supply the 2nd
11
q)addm[2;;][2;][6] / fix the 1st, then the 2nd, then supply the 3rd
10
Redefining the original function later does not affect projections already created from it:
q)f: {x*y}
q)g: f[3;] / triple
q)g 5
15
q)f: {x%y}
q)g 5 / still triple
15
Use placeholders (;) for omitted arguments to make the projection explicit:
q)foo: {x+y+z}
q)goo: foo[2] / discouraged
q)goo: foo[2;;] / recommended
As with functions, projections have their own data type:
q)f: {[x; y] x+y}
q)type f
100h
q)type f[1;]
104h
Function composition¶
Composition builds a new function that feeds one function's result into another — \(f(g(x))\) — and is written with the compose operator ('). The left value must be unary; the right may have any rank, and the composition takes the rank of the right one:
q)f: {2*x}
q)ff: {[w; x; y; z] w+x+y+z}
q)'[f; ff][1; 2; 3; 4] / f ff[1; 2; 3; 4]
20
Assigning a composition needs parentheses, so that the parser reads ' as part of the value instead of as an iterator applied to what follows:
q)d: ('[f; ff])
q)d[1; 2; 3; 4]
20
q)type d
105h
Compose a list of functions¶
Where every value is unary, Apply At (@) is shorter. Chain them with @ and elide all but the last:
q)tc: til count@ / indexes of a list
q)tc "abc"
0 1 2
q)(neg first reverse@) 10 20 30 40 50 / negate the first item of the reversal
-50
Extend compose with over to collapse a whole list of functions into one. Write the compose operator as '[;] to resolve its overloads, and keep the noun syntax in parentheses:
q)g: 10*
q)dd: ('[;]) over (g; f; ff)
q)dd[1; 2; 3; 4] / 10 * 2 * 1+2+3+4
200
q)'[;]/[(g; f; ff)][1; 2; 3; 4] / the same, with over written as /
200
Only the last function in the list may have rank above 1. The others are unary and apply to its result from right to left, so the leftmost function runs last.
Variadic functions¶
A variadic function accepts a variable number of arguments. enlist is the only built-in that is natively variadic, and the only one that accepts more than 8 arguments:
q)enlist[1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
1 2 3 4 5 6 7 8 9 10
That property is the foundation of every custom variadic function: compose your logic with enlist, and all the arguments arrive as a single list.
Put enlist on the right of a composition and the result is variadic: the composition inherits the rank of its right-hand value, and enlist is the one value in q whose rank is not fixed. However many arguments the caller passes, the unary function on the left receives them as a single list.
variadicFn: ('[{ ... }; enlist])
As a concrete example, take the future value of an investment:
Declaring all four parameters gives an ordinary function of fixed rank:
futval: {[p:`j; r:`f; y:`j; n:`j]
p * (1 + r % n) xexp n * y}
The compounding frequency \(n\) defaults to 12 (monthly), but callers may need to override it. Python covers this with a default argument; q needs the composition:
q
futval: ('[{[params]
if[not count[params] in 3 4;
'"futval accepts 3 or 4 parameters, but received ", string count params];
(p:`j; r:`f; y:`j): 3#params;
(n:`j): $[3 = count params; 12; last params];
p * (1 + r % n) xexp n * y
}; enlist])
q)futval[100; 0.07; 30] / default n=12
811.6497
q)futval[100; 0.002; 30; 365] / override n=365
106.1836
Python
>>> def futval(p, r, y, n=12):
... return p * (1 + r/n) ** (n*y)
...
>>> futval(100, 0.07, 30)
811.6497475359678
>>> futval(100, 0.002, 30, 365)
106.18363719971067
The limitation of this approach is that a variadic function does not project the way a function of fixed rank does. Without an elision, supplying fewer arguments is a call, which validation rejects. An elision does project, but the projection has a fixed rank, so the optional parameter can no longer get through:
q)futval[100; 0.07] / a call with two arguments, not a projection
'futval accepts 3 or 4 parameters, but received 2
[0] futval[100; 0.07]
^
q)type futval[100; 0.07;] / an elision does project
104h
q)futval[100; 0.07;][30] / the projection takes the remaining parameter
811.6497
q)futval[100; 0.07;][30; 365] / but it is fixed at rank 1
'rank
[0] futval[100; 0.07;][30; 365]
^
Function metadata¶
value takes a lambda apart. The head of the result describes the signature and the scope it closed over, and the last four items are the fully qualified name, source file, line number, and source text:
q)f: {[a; b] d:: neg c: a*b+5; c+e}
q)v: value f
q)v 1 / parameters
`a`b
q)v 2 / local variables
,`c
q)v 3 / namespace, then the globals referenced
``d`e`
q)(-4#v) 0 / fully qualified name ("..f" in the root namespace)
"..f"
q)last v / source text
"{[a; b] d:: neg c: a*b+5; c+e}"
The name is set on the first global assignment, so it carries the namespace of the definition, and the file and line locate the script the function was loaded from:
q)value[.util.scale] 1
`x`factor
q)(-4#value .util.scale) 0
".util.scale"
q)(-2#value .util.scale) 0 / line number in the file
2
A lambda that was never assigned to a name reports () instead.
The layout of value on a lambda can change between versions
The number of items in the middle varies with the constants in the function, so index from the end as above, and treat the result as a debugging aid rather than a stable API.
Next Steps¶
- For a deeper dive into functions, read Q for Mortals §6. Functions, and §6.9 Composition for composition in particular.
- Learn how pattern matching unpacks arguments and validates them with type checks and filter functions.
- Let iterators apply your functions across lists, dictionaries, and tables instead of writing loops.
- Collect your functions into reusable scripts, and into modules when you publish an API.