With our April release coming out, you may have noticed some major changes. Indeed, we have done a major overhaul of our libraries with our 0.6 release in April. The restructuring and more consistent naming hopefully make it easier and more intuitive to work with our vast arsenal of tools. At the same time, we have taken the opportunity to smooth some of the rougher edges in the language as well, and have introduced capabilities that make it easier to concisely express quantum algorithms.
This is therefore a good time to recap the language features we have introduced over the last couple of months, elaborate a little bit on the newest changes, and peek into what is coming next.
Past and Present
Let’s look at a possible implementation for one of the tasks in our quantum phase estimation kata. The code below implements a quantum phase estimation with two-bit precision. I’ll refer to the kata for further explanation on the algorithm and implementation.
namespace PhaseEstimationExample { open Microsoft.Quantum.Primitive; open Microsoft.Quantum.Canon; operation ApplyToSuperposition<'T> ( unitary : ('T => Unit : Adjoint, Controlled), controls : Qubit[], targets : 'T) : Unit { body { for (i in 0 .. Length(controls) - 1) { H (controls[i]); } (Controlled (unitary))(controls, targets); for (i in 0 .. Length(controls) - 1) { H (controls[i]); } } adjoint auto; controlled auto; controlled adjoint auto; } operation TwoBitPE ( unitary : (Qubit => Unit : Adjoint, Controlled), statePrep : (Qubit => Unit : Adjoint)) : Double { mutable gotZero = false; mutable gotOne = false; using (qs = Qubit[2]) { let control = qs[0]; let eigenstate = qs[1]; statePrep(eigenstate); mutable iter = 0; repeat { ApplyToSuperposition(unitary, [control], eigenstate); if (MResetZ(control) == Zero) { set gotZero = true; } else { set gotOne = true; } } until (iter >= 10 || gotZero && gotOne) fixup { set iter = iter + 1; } Reset(eigenstate); } if (!gotOne) { return 0.0; } if (!gotZero) { return 0.5; } mutable eigenvalue = -1.0; using (qs = Qubit[2]) { let control = qs[0]; let eigenstate = qs[1]; statePrep(eigenstate); ApplyToSuperposition(unitary, [control], eigenstate); ApplyWithCA(H, S, control); if (MResetZ(control) == Zero) { set eigenvalue = 0.75; } else { set eigenvalue = 0.25; } Reset(eigenstate); } return eigenvalue; } }
namespace PhaseEstimationExample { open Microsoft.Quantum.Intrinsic; open Microsoft.Quantum.Measurement; open Microsoft.Quantum.Canon as Canon; operation ApplyToSuperposition<'T> ( unitary : ('T => Unit is Adj + Ctl), controls : Qubit[], targets : 'T) : Unit is Adj + Ctl { for (q in controls) { // alternatively, leverage the operations implemented in the Canon H (q); } Controlled unitary(controls, targets); for (q in controls) { H (q); } } operation TwoBitPE ( unitary : (Qubit => Unit is Adj + Ctl), statePrep : (Qubit => Unit is Adj)) : Double { using ((control, eigenstate) = (Qubit(), Qubit())) { statePrep(eigenstate); mutable (gotZero, gotOne) = (false, false); mutable iter = 0; repeat { ApplyToSuperposition(unitary, [control], eigenstate); let meas = MResetZ(control); set (gotZero, gotOne) = (gotZero or meas == Zero, gotOne or meas == One); } until (iter >= 10 or gotZero and gotOne) fixup { set iter += 1; } Reset(eigenstate); if (not gotZero or not gotOne) { return gotOne ? 0.5 | 0.0; } } using ((control, eigenstate) = (Qubit(), Qubit())) { statePrep(eigenstate); ApplyToSuperposition(unitary, [control], eigenstate); Canon.ApplyWithCA(H, S, control); let eigenvalue = MResetZ(control) == Zero ? 0.75 | 0.25; Reset(eigenstate); return eigenvalue; } } }
This example illustrates some of the enhancements leading up to and including our 0.6 release.
The example above makes use of some of the added ergonomic features, like the capability to loop over items in an array, or to unify multiple allocations in a single using- or borrowing-statement. In addition to qubit arrays, single qubits and any nested tuple of qubits or qubit arrays may be allocated. The binding of the declared variable names follows the same deconstruction rules as any variable assignment in Q#. The same deconstruction rules thus apply for the bindings in let-, mutable-, set-, using- and borrowing-statements, as well as for the loop variable(s) in for-loops.
Looking in particular at ApplyToSuperposition demonstrates one of the capabilities added in our 0.6 release. Already the 0.3 release last fall made it possible to implement the body of an operation directly within the operation declaration like it is done for functions – as long as no other specialization needs to be declared explicitly. Our 0.6 release further increases the benefits of this feature by allowing to omit explicit specialization declarations if they can be auto-generated by the compiler. The annotation is Adj + Ctl indicates that the operation is adjointable and controllable, and the corresponding specializations are auto-generated. This annotation may be incomplete or missing, and the compiler will infer the operation characteristics based on both the annotation as well as all explicitly defined specializations.
A closer look at the TwoBitPE operation shows two other handy features that have been added: conditional expressions, and the capability to return from within using- and borrowing- blocks. You are probably familiar with conditional expressions from C#. Aside from the slightly different syntax, the ones in Q# work the exact same way, including the guarantee that only the expression in the applicable of the two cases will be evaluated. This feature greatly reduces the need for mutable variables, along with the new capability to return directly from within using- and borrowing statements. To facilitate optimizations for the execution on quantum hardware, such return-statements are only permissible as the last statement of the allocation scope for a certain execution path.
Given the breaking changes in our libraries that are part of this release, we have furthermore taken the opportunity to clarify the concept of mutability within Q#. With arrays – like all Q# types – being value types, this entails a breaking change in syntax for array items in set-statements. This change and some of the considerations and ideas behind it are discussed in more detail in the section below.
The change in particular facilitates enabling two new capabilities that we believe are going to be quite useful: apply-and-reassign statements as well as copy-and-update expressions. The example shown above uses an apply-and-reassign statement to increment the value of the counter iter in each iteration of the repeat-until-success-loop. Similar statements are available for all binary operators in which the type of the left-hand-side matches the expression type.
Copy-and-update expressions allow to construct a new array based on an existing one where only certain items are modified. They are implemented as ternary operators, and the corresponding update-and-reassign statement replaces the current functionality that permitted array items on the left-hand-side of a set-statement. I’ll refer to the next section for more details and code examples.
Last but not least, we have added the capability to define short names for namespaces as part of open directives. In the phase estimation example, Canon is used as a short name for the Microsoft.Quantum.Canon namespace. Namespace short names come in handy to make the namespace of a type or callable explicit without having to use the lengthy namespace name.
The following list summarizes some of the capabilities and language features outlined above:
- namespace short names
- implementation within declaration for operations
- partial specialization inference
- support for tuple deconstruction in all bindings
- deconstruction into loop variables when looping over arrays
- single qubit and tuple allocations
- return within using and borrowing
- apply-and-reassign statements
- copy-and-update expressions
- conditional expressions
- reduced need for parentheses
On the Concept of Mutability
Q# provides the means to define mutable variables, i.e., symbols that can be reassigned. Mutability within Q# is a concept that applies to a symbol rather than a type or value. Put more broadly, the concept of mutability applies to the “handle” that allows to access a value rather than to the value itself. Specifically, it does not have a representation in the type system, implicitly or explicitly, and whether or not a binding is mutable (as indicated by the mutable keyword) or immutable (as indicated by let ) does not change the type of the bound variable(s). The behavior of a certain value is thus the same independent on whether it is accessed via a mutable or immutable handle, as it is determined entirely by its type.
Since arrays are value types, this more specifically implies that a statement of the form set arr[i] = 0; implicitly incorporates copying the array bound to a mutable variable arr , except for item i which is set to 0. The newly constructed array is subsequently rebound to the same symbol arr . Under the hood, such a pattern is optimized to avoid unnecessary copies. In particular, the modification is done in-place as long as no other symbol besides arr is bound to the same value.
The current syntax, while convenient and familiar looking, heavily implies a reference type behavior consistent with in-place item updates in other languages. Since this intuition is misleading, we are introducing a clear separation between symbol reassignment and the described copy-and-update mechanism with Q# version 0.6. This comes with the added benefit of being able to express such array modifications as expressions.
function EmbedPauli (pauli : Pauli, location : Int, n : Int) : Pauli[] { mutable pauliArray = new Pauli[n]; for (index in 0 .. n - 1) { let item = index == location ? pauli | PauliI; set pauliArray w/= index <- item; } return pauliArray; }
In this example we have already replaced the no longer supported syntax set pauliArray[index] = item; with the more explicit set pauliArray w/= index <- item;, where w/ should be read as abbreviation for “with”. The introduced update-and-reassign statement of the form set <id> w/= <expr1> <- <expr2>; closely resembles the newly supported apply-and-reassign statements for binary operators described in the previous section both in syntax and semantic. Consistent with the mechanism for item access and slicing of arrays, <expr1> may either be of type Int or Range , with the corresponding implications for the type requirement on <expr2> .
function EmbedPauli (pauli : Pauli, i : Int, n : Int) : Pauli[] { return ConstantArray(n, PauliI) w/ i <- pauli; }
This illustrates how making use of new language features as well as the library tools provided in Microsoft.Quantum.Arrays can reduce both code verbosity and the need to define mutable variables. And with that, let’s move on to take a peek at some additional capabilities that are coming in shortly.
Peek Preview
Let me finally delve a little bit into what upcoming additions to Q# you can expect in the near future, and more specifically into the following features:
- named items for user-defined types
- while-loops within functions
- optional fixup block
Named items for user-defined types
newtype Complex = (Re : Double, Im : Double); function Addition (c1 : Complex, c2 : Complex) : Complex { return Complex(c1::Re + c2::Re, c1::Im + c2::Im); }
newtype ComplexArray = (Count : Int, Data : Complex[]); function AsComplexArray (data : Double[]) : ComplexArray { mutable res = ComplexArray(0, new Complex[0]); for (item in data) { set res w/= Data <- res::Data + [Complex(item, 0.)]; // update-and-reassign statement } return res w/ Count <- Length(res::Data); // returning a copy-and-update expression }
While-loops and optional fixup-block
Let’s also take a brief look at some control flow statements in Q#. Some of you may have noticed that we have started to give warnings when using repeat-until-success loops in functions. On one hand, repeat-until-success patterns are widely used in particular classes of quantum algorithms – hence the dedicated language construct in Q#. On the other hand, loops that break based on a condition and whose execution length is thus unknown at compile time need to be handled with particular care in a quantum runtime. For now, we therefore to some degree want to discourage the use of such loops, such that other more familiar looking loop constructs are missing in Q#. However, there is no particular reason to not support such loops in functions, given that they can only contain classical computations. We are hence taking a step towards further discriminating the functionality of operations versus functions, and will support using while-loops in functions only.
On a similar note, we are also lifting the requirement that the fixup-block in repeat-until-success loops is mandatory even if it does not contain any statements. In future releases you will be able to omit it in this case and terminate the statement with a semicolon after the until-clause.
0 comments