Higher-order functions¶
Recursive QExpr
functions are extremely effective at producing reusable
quantum kernel expressions that can map over arbitrary compile-time QList
or
DataList
values. However, in many cases these recursive functions can
result in significant boilerplate code, and patterns emerge common to functional
programming.
For example, one of the most common idioms in recursive QExpr
functions is mapping
a particular QExpr
function over a QList
, applying it to each
qubit in a sequential join. Consider the following recursive QExpr
function
that applies a PrepZ_
gate to every qubit in a QList
:
QExpr prepAll_recursive(qlist::QList qs) {
return qexpr::cIf(qs.size() > 0,
qexpr::_PrepZ(qs[0]) + prepAll_recursive(qs + 1), // qs non-empty
qexpr::identity() // qs empty
);
}
Using C++ function pointers, it is possible to write higher-order functions—that is, functions that take as input other function pointers—to reduce this boilerplate overhead.
For convenience, the library qexpr_utils.h
provides several examples of
higher-order functions.
For instance, the following function, map1
, takes as input a function
pointer and applies it to each qubit in a QList
. The recursive structure of
map1
is identical to that of prepAll_recursive
, just with an extra
function pointer parameter.
// qexpr_utils.h
template<typename QExprFun>
QExpr qexpr::map1(QExprFun f, qlist::QList qs) {
return qexpr::cIf(qs.size() > 0,
f(qs[0]) + qexpr::map1(f, qs + 1), // qs non-empty
qexpr::identity() // qs empty
);
}
This templated function applies a function f
, which takes a qbit
and
returns a QExpr
, to every qbit
in a QList
, and returns the join of
all the results. Then, instead of a user writing the function
prepAll_recursive()
, they can simply invoke map1(qexpr::_PrepZ, qs)
to
prepare all the qubits in qs
.
Not all recursive QExpr
functions fit exactly into this pattern. For
example, suppose a user wants to apply rotation gates to each qubit in a
QList
, with different rotation arguments for each qubit. The relevant
recursive function would need to map over both the QList
argument and an
array of rotation parameters, for example:
// Assume that params is an array of doubles of size qs.size()
QExpr RZAll_recursive(qlist::QList qs, double* params) {
return qexpr::cIf(qs.size() > 0,
qexpr::_RZ(qs[0], params[0]) + RZAll_recursive(qs+1, params+1),
qexpr::identity()
);
}
Because _RZ
does not take a single qubit argument, it is not possible to apply map1
directly. However,
the qexpr_utils.h
library also supplies a more general map
function, which
takes as input a function f
that takes in any number of arguments, the first
of which is a qbit
. The map
function then expects (1) a QList
to map
over; (2) some number of QList
or array arguments (it applies f
to every
element in the array); and (3) some number of scalar arguments, which it passes
directly to f
.
// qexpr_utils.h
template<typename QExprFun, typename... Args>
QExpr qexpr::map(QExprFun f, qlist::QList qs, Args... args) noexcept;
Instead of RZAll_recursive(qs, params)
, a user can just apply
qexpr::map(qexpr::_RZ, qs, params)
to obtain the same result.
This map
function can be used in several ways:
Like
map1
, can be used to map a single-qubit gate over aQList
:
qexpr::map(qexpr::_PrepZ,qs)
Map a multi-qubit gate, like
CNOT
, over twoQList
s:
qexpr::map(qexpr::_CNOT, qs1, qs2)
Map a single-qubit gate that accepts parameters, e.g.
RZ
, over aQList
, with the same scalar parameter applied to each argument:
qexpr::map(qexpr::_RZ,qs,M_PI/2)
Map a single-qubit gate with parameters, like
RZ
, over (1) aQList
and (2) an array of rotation parameters:
double params[3] = {M_PI/2, M_PI/4, M_PI/8}; qexpr::eval_hold(qexpr::map(qexpr::_RZ, qs, params));
Local qubits¶
Like quantum_kernel
functions, QExpr
functions can use both local and global
qubits. However, while top-level quantum_kernel
functions cannot accept qubit
arguments, there is no such restriction for quantum kernel expressions. Qubits
or QList
values can now be declared locally in a non-quantum function and
passed to top-level QExpr
functions. For example:
int main() {
qbit q;
bool b;
eval_hold(_PrepZ(q) + _H(q) + _MeasZ(q, b));
}
The underlying reason for this is that every evaluation call to eval_hold
or
eval_release
from a (classical) function foo()
tells the compiler to
treat foo()
as a quantum_kernel
function. Because
local qubits can be declared inside quantum_kernel
functions, they can therefore
be declared in classical functions with evaluation calls.
If local qubits are declared within a QExpr
-returning function, it is best practice
to use the PROTECT
attribute; see Known limitations.