Operators
A function in Qmod can take other functions as arguments and call these functions in its body. Functions operating on other functions are often referred to as higher-order functions, or operators. This mechanism is used to define reusable quantum algorithm patterns, such as the Quantum Phase Estimation and the Grover operator.
Function types
Parameters of function types are declared like parameters of other type categories. The function type determines the list of arguments that must be accepted by the function passed in as argument. Array function types correspond to an indexable collection of function values with a common signature.
Syntax
Function type syntax has the following form -
qfunc ( function-type-parameters )
function-type-parameters is a list of zero or more comma-separated declarations in the form - [ name : ] type.
If [] follows the qfunc keyword, the parameter is interpreted as a function array -
qfunc [ ] ( function-type-parameters )
The QCallable
type hint is used to specify a function type. QCallable
itself is a
generic class, taking as parameters a list of type hints that declare the parameters of
the function.
The QCallableList
type hint specifies a function array type.
The name of a parameter in a function type can optionally be specified using the form - Annotated [ type , " name "].
Semantics
- The function passed as argument to an operator must agree in its signature with the number, types, and order of parameters declared in the respective function type. Note that names of parameters are optional in the function type, and where specified, are not required to match the argument.
- A parameter of a function type can be called inside the function's body just like a regular function, passing arguments as per the declared signature.
- An element of a function array type can be called with a subscript operator applied to the parameter, followed by the argument list.
Example
In the following example, the function my_operator
declares the parameter my_operand
of a function type with one classical parameter and one quantum parameter. The function
is called twice in its body, passing different argument values. In function
main
, my_operator
is called twice, each time passing a different function as its
argument.
qfunc my_operator(my_operand: qfunc (theta: real, target: qbit), q: qbit) {
my_operand(pi / 2, q);
my_operand(pi / 4, q);
}
qfunc main() {
q: qbit;
allocate(1, q);
my_operator(RX, q);
my_operator(RY, q);
}
from classiq import CReal, QBit, QCallable, RX, qfunc, allocate
from classiq.qmod.symbolic import pi
@qfunc
def my_operator(my_operand: QCallable[CReal, QBit], q: QBit) -> None:
my_operand(pi / 2, q)
my_operand(pi / 4, q)
@qfunc
def main() -> None:
q = QBit("q")
allocate(1, q)
my_operator(lambda theta, target: RX(theta, target), q)
my_operator(lambda theta, target: RY(theta, target), q)
Notes:
- Passing named functions in Python is currently not supported. This example
uses Python lambda expressions - see more in the next section.
- Function arguments in Python do not support specifying argument names. When
translating Qmod Python description to native syntax, names arg0
,
arg1
, etc. are associated with the arguments automatically.
Synthesizing this model creates the quantum program shown below. You can see four rotations in the circuit with their respective angles.
Lambda functions
You can pass a function to an operator in one of two forms - a named function, and a lambda function. Lambda functions are anonymous functions defined in-line in the operator call site. Note that in Python only the lambda function form is supported.
Syntax
Lambda function syntax is somewhat similar to a function definition. The
keyword qfunc
is replaced with lambda
, the name of the function is omitted,
and argument lists only specify only names, not types.
lambda [ < classical-arg-names > ] ( quantum-arg-names ) { statements }
A Python Callable
object is used as a Qmod lambda function. This can take one of two forms -
a Python lambda expression, or a named Python function (not decorated with @qfunc
).
When a named function is used with type hints on its arguments, the names of the
operands will be reflected in the Qmod description.
Example 1
Consider the following snippet, where my_operator
is called twice from function main
,
once with a regular function and a second time with a lambda function. These two calls
are equivalent.
qfunc my_operator(my_operand: qfunc (angle: real, target: qbit), q: qbit) {
H(q);
my_operand(pi / 2, q);
}
qfunc my_operand(angle: real, target: qbit) {
RX(angle, target);
}
qfunc main() {
q: qbit;
allocate(1, q);
my_operator(my_operand, q);
my_operator(lambda (angle, target) {
RX(angle, target);
}, q);
}
from classiq import CReal, H, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def my_operator(my_operand: QCallable[CReal, QBit], q: QBit) -> None:
H(q)
my_operand(pi / 2, q)
def my_operand(angle: CReal, target: QBit) -> None:
RX(angle, target)
@qfunc
def main() -> None:
q = QBit("q")
allocate(1, q)
my_operator(my_operand, q)
my_operator(lambda angle, target: RX(angle, target), q)
Note that in the first call, the argument is a regular Python function, not decorated
with @qfunc
.
Example 2
An operator may pass expressions involving its own arguments to its operand. The following example demonstrates this.
qfunc foo_operator(n: int, my_operand: qfunc (angle: real, qb: qbit), qba: qbit[2]) {
H(qba[0]);
my_operand(pi / n, qba[1]);
}
qfunc main() {
qba: qbit[];
allocate(2, qba);
foo_operator(4, lambda(angle, qb) {
RX(angle, qb);
}, qba);
}
from classiq import CInt, CReal, H, QArray, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def foo_operator(
n: CInt,
my_operand: QCallable[CReal, QBit],
qba: QArray[QBit, 2],
) -> None:
H(qba[0])
my_operand(pi / n, qba[1])
@qfunc
def main() -> None:
qba = QArray("qba")
allocate(2, qba)
foo_operator(n=4, my_operand=lambda theta, target: RX(theta, target), qba=qba)
Synthesizing this model creates the quantum program shown below. You can see that the call to
foo_operator
applies an X rotation on qubit 1, based on the value of n
passed to it.
Capturing context variables and parameters
A lambda function that is passed as an argument to an operator may reference classical or quantum variables in its own lexical scope. The objects whose references are captured are available when the callable is invoked by the operator, even though the operator itself is oblivious to them.
An operator must not implicitly change the initialized status of quantum variables captured inside lambda functions that are passed to it. Only initialized variables may be captured, and they remain initialized after the operator call. Hence, quantum variables cannot be captured as output-only or input-only arguments inside a lambda function. Note that an operator may actually invoke the operand once, multiple times, or not at all.
Example
The following example is similar to Example 2 from the previous section. However, in this case, the quantum variable used inside the lambda function is captured directly from the scope rather than being passed to it indirectly through the operator. The resulting quantum program in this case is identical to that of the previous version in Example 2 above.
qfunc foo_operator(n: int, my_operand: qfunc (angle: real), qb: qbit) {
H(qb);
my_operand(pi / n);
}
qfunc main() {
qb1: qbit;
qb2: qbit;
allocate(1, qb1);
allocate(1, qb2);
foo_operator(4, lambda(t) {
RX(t, qb1);
}, qb2);
}
from classiq import CInt, CReal, H, QBit, QCallable, RX, allocate, qfunc
from classiq.qmod.symbolic import pi
@qfunc
def foo_operator(
n: CInt,
my_operand: QCallable[CReal],
qb: QBit,
) -> None:
H(target=qb)
my_operand(pi / n)
@qfunc
def main() -> None:
qb1 = QBit("qb1")
qb2 = QBit("qb2")
allocate(1, qb1)
allocate(1, qb2)
foo_operator(n=4, my_operand=lambda t: RX(theta=t, target=qb1), qb=qb2)