Specialization declarations
As explained in the section about callable declarations, there is currently no reason to explicitly declare specializations for functions. This topic applies to operations and elaborates on how to declare the necessary specializations to support certain functors.
It is quite a common problem in quantum computing to require the adjoint of a given transformation. Many quantum algorithms require both an operation and its adjoint to perform a computation.
Q# employs symbolic computation that can automatically generate the corresponding adjoint implementation for a particular body implementation. This generation is possible even for implementations that freely mix classical and quantum computations. There are, however, some restrictions that apply in this case. For example, auto-generation is not supported for performance reasons if the implementation makes use of mutable variables. Moreover, each operation called within the body generates the corresponding adjoint needs to support the Adjoint
functor itself.
Even though one cannot easily undo measurements in the multi-qubit case, it is possible to combine measurements so that the applied transformation is unitary. In this case, it means that, even though the body implementation contains measurements that on their own don't support the Adjoint
functor, the body in its entirety is adjointable. Nonetheless, auto-generating the adjoint implementation will fail in this case. For this reason, it is possible to manually specify the implementation.
The compiler automatically generates optimized implementations for common patterns such as conjugations.
Nonetheless, an explicit specialization may be desirable to define a more optimized implementation by hand. It is possible to specify any one implementation and any number of implementations explicitly.
Note
The correctness of such a manually specified implementation is not verified by the compiler.
In the following example, the declaration for an operation SWAP
, which exchanges the state of two qubits q1
and q2
, declares an explicit specialization for its adjoint version and its controlled version. While the implementations for Adjoint SWAP
and Controlled SWAP
are thus user-defined, the compiler still needs to generate the implementation for the combination of both functors (Controlled Adjoint SWAP
, which is the same as Adjoint Controlled SWAP
).
operation SWAP (q1 : Qubit, q2 : Qubit) : Unit
is Adj + Ctl {
body ... {
CNOT(q1, q2);
CNOT(q2, q1);
CNOT(q1, q2);
}
adjoint ... {
SWAP(q1, q2);
}
controlled (cs, ...) {
CNOT(q1, q2);
Controlled CNOT(cs, (q2, q1));
CNOT(q1, q2);
}
}
Auto-generation directives
When determining how to generate a particular specialization, the compiler prioritizes user-defined implementations. This means that if an adjoint specialization is user-defined and a controlled specialization is auto-generated, then the controlled adjoint specialization is generated based on the user-defined adjoint and vice versa. In this case, both specializations are user-defined. As the auto-generation of an adjoint implementation is subject to more limitation, the controlled adjoint specialization defaults to generating the controlled specialization of the explicitly defined implementation of the adjoint specialization.
In the case of the SWAP
implementation, the better option is to adjoint the controlled specialization to avoid unnecessarily conditioning the execution of the first and the last CNOT
on the state of the control qubits.
Adding an explicit declaration for the controlled adjoint version that specifies a suitable generation directive forces the compiler to generate the controlled adjoint specialization based on the manually specified implementation of the controlled version instead. Such an explicit declaration of a specialization that is to be generated by the compiler takes the form
controlled adjoint invert;
and is inserted inside the declaration of SWAP
.
On the other hand, inserting the line
controlled adjoint distribute;
forces the compiler to generate the specialization based on the defined (or generated) adjoint specialization. See this partial specialization inference proposal for more details.
For the operation SWAP
, there is a better option. SWAP
is self-adjoint, that is, it is its own inverse; the -defined implementation of the adjoint merely calls the body of SWAP
. You express this with the directive
adjoint self;
Declaring the adjoint specialization in this manner ensures that the controlled adjoint specialization that is automatically inserted by the compiler merely invokes the controlled specialization.
The following generation directives exist and are valid:
Specialization | Directive(s) |
---|---|
body specialization: |
- |
adjoint specialization: |
self , invert |
controlled specialization: |
distribute |
controlled adjoint specialization: |
self , invert , distribute |
That all generation directives are valid for a controlled adjoint specialization is not a coincidence; as long as functors commute, the set of valid generation directives for implementing the specialization for a combination of functors is always the union of the set of valid generators for each one.
In addition to the previously listed directives, the directive auto
is valid for all specializations except body
; it indicates that the compiler should automatically pick a suitable generation directive.
The declaration
operation DoNothing() : Unit {
body ... { }
adjoint auto;
controlled auto;
controlled adjoint auto;
}
is equivalent to
operation DoNothing() : Unit
is Adj + Ctl { }
The annotation is Adj + Ctl
in this example specifies the operation characteristics, which contain the information about what functors a particular operation supports.
While for readability's sake, it is recommended that you annotate each operation with a complete description of its characteristics, the compiler automatically inserts or completes the annotation based on explicitly declared specializations. Conversely, the compiler also generates specializations that haven't been declared explicitly but need to exist based on the annotated characteristics. We say the given annotation has implicitly declared these specializations. The compiler automatically generates the necessary specializations if it can, picking a suitable directive. Q# thus supports inference of both operation characteristics and existing specializations based on (partial) annotations and explicitly defined specializations.
In a sense, specializations are similar to individual overloads for the same callable, with the caveat that certain restrictions apply to which overloads you can declare.