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 twoQLists:
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) aQListand (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.