Skip to content

How to control execution

This page explains how to express conditional logic and iteration in q: the branching constructs, the two conditional operators and the difference between them, the loop keywords, the iterators that usually replace them, early return, and trapping errors.

q applies an operation to a whole list at once, so most logic that other languages write as a loop over elements is written in q without any control flow at all. That is both simpler and faster, and it is the first thing to reach for.

The classic constructs — if, do, while and the conditional operators — are all there for the cases that genuinely need them. But note that "each iteration depends on the previous one" is not one of those cases: that is what the iterators over and scan are for.

if

The if keyword evaluates one or more expressions when a condition is true. Here any reduces a vector to the single boolean the condition needs:

q

q)sizes:100 250 0 75
q)if[any sizes=0; show "warning: zero-size trades"]
"warning: zero-size trades"

Python

>>> sizes = [100, 250, 0, 75]
>>> if any(s == 0 for s in sizes):
...     print("warning: zero-size trades")
...
warning: zero-size trades

Note where the element-wise work happens. sizes=0 compares the whole vector in one operation and any collapses the result, whereas Python iterates the list an element at a time. When nothing matches, the condition is false and if evaluates nothing:

q)sizes:100 250 50 75
q)if[any sizes=0; show "warning: zero-size trades"]
q)

if does not introduce a scope. A name assigned inside the brackets is visible outside them, and a name in a branch that does not run is never created at all:

q)if[1b; x:33; show "true"]
"true"
q)x
33
q)if[0b; y:33; show "true"]
q)y
'y
  [0]  y
       ^

if returns the generic null, so it is a statement rather than an expression — use $ below when you want a value:

q)r:if[1b; x:33; x:55]
q)r~(::)                      / compare the result with the generic null
1b

Conditional evaluation with $

One of the overloads of $ is conditional evaluation.

Three expressions

With three arguments, $ is q's if-then-else as an expression: it returns the value of whichever branch runs.

q

q)x:0
q)$[x=0; "x is zero"; "x is not zero"]
"x is zero"
q)x:1
q)$[x=0; "x is zero"; "x is not zero"]
"x is not zero"

Python

>>> x = 0
>>> "x is zero" if x == 0 else "x is not zero"
'x is zero'
>>> x = 1
>>> "x is zero" if x == 0 else "x is not zero"
'x is not zero'

Testing a number against zero is redundant, because a number is itself a valid condition. Swapping the branches lets you drop the comparison:

q)x:0
q)$[x; "x is not zero"; "x is zero"]
"x is zero"

As with if, the brackets are not a scope:

q)x:0
q)$[x=0; y:22; z:33]
22
q)y
22
q)z
'z
  [0]  z
       ^

More than three expressions

Nested conditionals flatten: write $[q;a;$[r;b;c]] as $[q;a;r;b;c], and repeat for further branches. So $[q;a;$[r;b;$[s;c;d]]] becomes:

$[q;a;    / if q, a
  r;b;    / else if r, b
  s;c;    / else if s, c
  d]      / else d

With an even number of expressions there is no final else, and a value is returned only if some condition holds — otherwise you get the generic null:

q)$[1b; `true; 1b; `foo]
`true
q)$[0b; `true; 1b; `foo]
`foo
q)$[0b; `true; 0b; `foo]        / no condition held
q)$[0b; `true; 0b; `foo]~(::)   / so the result is the generic null
1b

What counts as true

A condition need not be a boolean. Any atom of an integral or floating type works, and zero is false while anything else is true:

q)$[2; `nonzero; `zero]
`nonzero
q)$[0; `nonzero; `zero]
`zero

Two cases are worth knowing. A null is not false — it is a non-zero value and so takes the true branch:

q)$[0N; `nonzero; `zero]
`nonzero

And a type with no notion of zero is rejected rather than coerced:

q)$[`sym; `a; `b]
'type

A null condition is true

$[0N; ...] and if[0N; ...] take the true branch, which is rarely what a test on possibly-missing data intends. Test explicitly — $[not null v; ...] — rather than relying on the value itself.

$ evaluates only the branch it takes

$ is lazy: the branch not taken is never evaluated. That is what makes it safe to guard an expression that would fail:

q)note:{show "ran ",x; x}
q)$[1b; note"then"; note"else"]
"ran then"
"then"

Only then ran. This matters when comparing $ with the vector conditional below, which behaves differently.

Vector conditional ?

? with three arguments chooses element by element between two alternatives, driven by a boolean vector. Where the condition is true the second argument supplies the element, where false the third does. Either may be an atom, which is reused for every position.

q

q)?[111000b; 1 2 3 4 5 6; 10 20 30 40 50 60]
1 2 3 40 50 60
q)?[111000b; 1 2 3 4 5 6; 10]
1 2 3 10 10 10
q)?[111000b; 1; 10 20 30 40 50 60]
1 1 1 40 50 60
q)?[111000b; 1; 10]
1 1 1 10 10 10

Python

>>> import numpy as np
>>> c = np.array([1,1,1,0,0,0], dtype=bool)
>>> np.where(c, [1,2,3,4,5,6], [10,20,30,40,50,60])
array([ 1,  2,  3, 40, 50, 60])
>>> np.where(c, [1,2,3,4,5,6], 10)
array([ 1,  2,  3, 10, 10, 10])
>>> np.where(c, 1, [10,20,30,40,50,60])
array([ 1,  1,  1, 40, 50, 60])

Any vector argument must match the length of the condition:

q)?[111b; 1 2 3 4 5 6; 10]
'length
  [0]  ?[111b; 1 2 3 4 5 6; 10]
       ^

? and $ are not interchangeable

They differ in two ways that matter in practice.

$ needs an atom condition. Handing it a vector is an error, which is the usual reason to reach for ?:

q)$[10b; 1; 2]
'type

? evaluates both branches, in full. Unlike $, it computes each alternative for every element and then selects:

q)?[10b; note"A"; note"B"]
"ran B"
"ran A"
"AB"

So ? selects between two results that are both already computable; it cannot be used to avoid work or to guard against a failure in one branch. This guard does not guard:

q)xs:4 0 2 0
q)strict:{if[x=0; '"divide by zero"]; 10 % x}
q)?[xs<>0; strict each xs; 0n]
'divide by zero

The division is attempted for every element, including the zeros the condition was meant to exclude. Where the operation merely produces a null or an infinity rather than signalling, the wasted work is silent:

q)10 % xs           / no error, but 0w computed for the zeros
2.5 0w 5 0w
q)?[xs<>0; 10 % xs; 0n]
2.5 0n 5 0n

Loops

do and while exist, but are rarely the right answer — prefer to vectorize, or to use an iterator.

do

do repeats expressions a fixed number of times.

q

q)do[2; show "loop"]
"loop"
"loop"
q)x:0
q)do[2; x:x+1; show "loop ", string x]
"loop 1"
"loop 2"

Python

>>> for _ in range(2):
...     print("loop")
...
loop
loop
>>> x = 0
>>> for _ in range(2):
...     x += 1
...     print(f"loop {x}")
...
loop 1
loop 2

To repeat code for timing rather than for effect, use \t, which takes a repeat count of its own.

while

while repeats expressions for as long as a condition stays non-zero.

q

q)f:5
q)while[f:f-1; show "loop", string f]
"loop4"
"loop3"
"loop2"
"loop1"

Python

>>> f = 5
>>> while (f := f - 1):
...     print(f"loop{f}")
...
loop4
loop3
loop2
loop1

Neither keyword introduces a scope, and both return the generic null:

q)do[2; x:33; show "loop"]
"loop"
"loop"
q)x
33
q)r:do[2; x:33]
q)r~(::)
1b

There is no break or continue

q has no way to leave a do or while early. The nearest equivalents are to fold the exit condition into the while test, to return early from the enclosing function, or to use the converge form of scan, which stops on a condition by construction.

Prefer an iterator to a loop

An iteration whose each step depends on the one before is the case people reach for while to express — and it is exactly what over and scan do, without the bookkeeping. scan keeps every intermediate result; over keeps only the last.

q

q)(+\) 1 2 3 4 5        / running total
1 3 6 10 15
q)(+/) 1 2 3 4 5        / final total
15
q){x*1.05}\[3;100f]     / 3 years of 5% growth
100 105 110.25 115.7625

Python

>>> from itertools import accumulate
>>> from functools import reduce
>>> list(accumulate([1,2,3,4,5]))
[1, 3, 6, 10, 15]
>>> reduce(lambda a, b: a + b, [1,2,3,4,5])
15
>>> list(accumulate(range(3), lambda x, _: x * 1.05, initial=100.0))
[100.0, 105.0, 110.25, 115.7625]

The converge forms replace a while loop directly: give scan a condition instead of a count and it iterates until the condition fails, returning each step.

q){x%2}\[{x>1};100f]    / halve while greater than 1
100 50 25 12.5 6.25 3.125 1.5625 0.78125

See over, scan and accumulators for the full set of forms.

Return early from a function

A bare : returns from the enclosing function immediately. This is what replaces the guard-clause style that other languages write with return:

q

classify:{[n]
  if[n<0; :`negative];
  if[n=0; :`zero];
  `positive }
q)classify each -5 0 7
`negative`zero`positive

Python

>>> def classify(n):
...     if n < 0:
...         return "negative"
...     if n == 0:
...         return "zero"
...     return "positive"
...
>>> [classify(n) for n in (-5, 0, 7)]
['negative', 'zero', 'positive']

: at the top level assigns, it does not return

: only returns from inside a function. Typed at the console or in a script outside a function, x:1 is an assignment — so a stray :expr in a script is a syntax error rather than an early exit.

Trap errors

Errors are control flow too. ' signals one, and @ and . have trap forms that catch it — q's equivalent of try/except.

@[f; x; handler] applies f to the single argument x; if it signals, handler is called with the error string instead:

q

q)@[{1+x}; 41; {"caught: ",x}]
42
q)@[{1+x}; `notanumber; {"caught: ",x}]
"caught: type"

Python

>>> def attempt(x):
...     try:
...         return 1 + x
...     except TypeError as e:
...         return f"caught: {e}"
...
>>> attempt(41)
42
>>> attempt("notanumber")
"caught: unsupported operand type(s) for +: 'int' and 'str'"

Use .[f; args; handler] for a function of more than one argument, and signal your own errors with ':

q)checkPositive:{if[x<=0; '"must be positive"]; sqrt x}
q)@[checkPositive; 16; {"caught: ",x}]
4f
q)@[checkPositive; -1; {"caught: ",x}]
"caught: must be positive"
q).[{x+y}; (1;`a); {"caught: ",x}]
"caught: type"

The handler need not be a function. A plain value is returned as the fallback, which is a compact way to supply a default:

q)@[{1+x}; `bad; 0N]
0N

For production error handling that also captures a stack trace, see .Q.trp and programmatic traps.

Next steps