Q# 0.6: Language Features and More

Bettina Heim

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;
    }
}
With the latest additions to Q# over the last couple of months, we can now express this in the following way:
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.

Let’s consider for example the following function that can be found in our libraries:
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> .

While the introduced syntax is marginally more verbose, albeit definitively less familiar looking, everything remains the same in terms of behavior.
For a lot of cases, our library functions should provide the necessary tools for array initialization and thus help to avoid having to update array items in the first place. Additionally, we can consistently extend the syntax for copy-and-update expressions to define a convenient way to express array modifications. The example above for instance can be expressed more concisely by making use of the newly supported copy-and-update expressions of the form <expr1> w/ <expr2> <- <expr3>:
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
So far, Q# has a very minimal type system. With the focus for quantum algorithm being more towards what should be achieved rather than on a problem representation in terms of data structures, we intend to keep a somewhat more functional perspective. At the same time we want to make it convenient to leverage the type system and related compile time checks to formulate robust code. In that spirit we are adding support for named items in user-defined types. Item names can be defined upon type declaration, and any mixture of named and anonymous items is possible. Such items can be accessed in a similar fashion as “numbered” items in arrays.
newtype Complex = (Re : Double, Im : Double); 

function Addition (c1 : Complex, c2 : Complex) : Complex {
    return Complex(c1::Re + c2::Re, c1::Im + c2::Im);
}
The same constructs that are supported for array items will also be supported for named items in user-defined types. In particular, copy-and-update expressions as well as update-and-reassign statements like the ones discussed above for arrays can be seamlessly extended to include named items in user-defined types as well. This is illustrated in the following somewhat contrived example:
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.

Your Thoughts

I hope you like the changes we have done so far! Stay tuned for the new features in the upcoming releases! Either way, as always, let us know your thoughts!

0 comments

Discussion is closed.

Feedback usabilla icon