Variables in Q#

Q# distinguishes between mutable and immutable symbols, or variables, which are bound/assigned to expressions. In general, the use of immutable symbols is encouraged because it allows the compiler to perform more optimizations.

The left-hand-side of a binding consists of a symbol tuple and the right-hand side of an expression.

Immutable Variables

You can assign a value of any type in Q# to a variable for reuse within an operation or function by using the let keyword.

An immutable binding consists of the keyword let, followed by a symbol or symbol tuple, an equals sign =, an expression to bind the symbol(s) to, and a terminating semicolon.

For instance:

let measurementOperator = [PauliX, PauliZ, PauliZ, PauliX, PauliI];

This assigns a particular array of Pauli operators to the variable name (or "symbol"), measurementOperator.

Note

In the previous example, there is no need to explicitly specify the type of the new variable, as the expression on the right-hand side of the let statement is unambiguous, and the compiler infers the correct type.

Variables defined using let are immutable, meaning that once you define it, you can no longer change it in any way. This allows for several beneficial optimizations, including optimization of the classical logic that acts on variables to be reordered for applying the Adjoint variant of an operation.

Mutable Variables

As an alternative to creating a variable with let, the mutable keyword creates a mutable variable that can be rebound after it is initially created by using the set keyword.

You can rebind symbols declared and bound as part of a mutable statement to a different value later in the code. If a symbol is rebound later in the code, its type does not change, and the newly bound value must be compatible with that type.

Rebinding of Mutable Symbols

You can rebind a mutable variable using a set statement. Such a rebinding consists of the keyword set, followed by a symbol or symbol tuple, an equals sign =, an expression to rebind the symbol(s) to, and a terminating semicolon.

The following are some examples of rebinding statement techniques.

Apply-and-Reassign Statements

A particular kind of set-statement, the apply-and-reassign statement, provides a convenient way of concatenation if the right-hand side consists of the application of a binary operator, and the result is to be rebound to the left argument to the operator. For example,

mutable counter = 0;
for (i in 1 .. 2 .. 10) {
    set counter += 1;
    // ...
}

increments the value of the counter counter in each iteration of the for loop. The previous code is equivalent to

mutable counter = 0;
for (i in 1 .. 2 .. 10) {
    set counter = counter + 1;
    // ...
}

Similar statements are available for all binary operators in which the type of the left-hand side matches the expression type. These statements provide a convenient way to accumulate values.

For example, supposing qubits is a register of qubits:

mutable results = new Result[0];   // results is an empty array of type Result[]
for (q in qubits) {
    set results += [M(q)];         // concatenate the outcome from measuring q to the existing array
    // ...
}
...                                // results contains the measurement outcomes from the whole register

Update-and-Reassign Statements

A similar concatenation exists for copy-and-update expressions on the right-hand side. Correspondingly, update-and-reassign statements exist for named items in user-defined types as well as for array items.

newtype Complex = (Re : Double, Im : Double);

function ComplexSum(reals : Double[], ims : Double[]) : Complex[] {
    mutable res = Complex(0.,0.);

    for (r in reals) {
        set res w/= Re <- res::Re + r; // update-and-reassign statement
    }
    for (i in ims) {
        set res w/= Im <- res::Im + i; // update-and-reassign statement
    }
    return res;
}

In the case of arrays, Microsoft.Quantum.Arrays in the Q# standard library provides the necessary tools for many common array initialization and manipulation needs, and thus helps avoid having to update array items in the first place.

Update-and-reassign statements provide an alternative if needed:

operation GenerateRandomInts(max : Int, nSamples : Int) : Int[] {
    mutable samples = new Double[0];
    for (i in 1 .. nSamples) {
        set samples += [RandomInt(max)];
    }
    return samples;
}

operation SampleUniformDistrbution(nSamples : Int, nSteps : Int) : Double[] {
    let normalization = 1. / IntAsDouble(nSteps);
    mutable samples = GenerateRandomInts(nSteps, nSamples);
    
    for (i in IndexRange(samples) {
        let value = IntAsDouble(samples[i]);
        set samples w/= i <- normalization * value; // update-and-reassign statement
    }
}

Using the library tools for arrays provided in Microsoft.Quantum.Arrays, you can, for example, easily define a function that returns an array of Pauli types where the element at index i takes a given Pauli value, and all other entries are the identity (PauliI).

Here are two definitions of such a function, with the second taking advantage of the tools at our disposal.

function PauliEmbedding(pauli : Pauli, length : Int, location : Int) : Pauli[] {
    mutable pauliArray = new Pauli[length];             // initialize pauliArray of given length
    for (index in 0 .. length - 1) {                    // iterate over the integers in the length range
        set pauliArray w/= index <-                     // change the value at index to input pauli or PauliI
            index == location ? pauli | PauliI;         // cond. expression evaluating to pauli if index==location and PauliI if not
    }    
    return pauliArray;
}

Instead of iterating over each index in the array, and conditionally setting it to PauliI or the given pauli, you can instead use ConstantArray from Microsoft.Quantum.Arrays to create an array of PauliI types, and then simply return a copy-and-update expression in which you've changed the specific value at index location:

function PauliEmbedding(pauli : Pauli, length : Int, location : Int) : Pauli[] {
    return ConstantArray(length, PauliI) w/ location <- pauli;
}

Tuple Deconstruction

In addition to assigning a single variable, you can use the let and mutable keywords - or any other binding construct, such as set - to unpack the contents of a tuple type. An assignment of this form is said to deconstruct the elements of that tuple.

If the right-hand side of the binding is a tuple, then you can deconstruct that tuple upon assignment. Such deconstructions can involve nested tuples, and any full or partial deconstruction is valid as long as the shape of the tuple on the right-hand side is compatible with the shape of the symbol tuple.

For example:

let (i, f) = (5, 0.1); // i is bound to 5 and f to 0.1
mutable (a, (_, b)) = (1, (2, 3)); // a is bound to 1, b is bound to 3
mutable (x, y) = ((1, 2), [3, 4]); // x is bound to (1,2), y is bound to [3,4]
set (x, _, y) = ((5, 6), 7, [8]);  // x is rebound to (5,6), y is rebound to [8]
let (r1, r2) = MeasureTwice(q1, PauliX, q2, PauliY);

Binding Scopes

In general, symbol bindings go out of scope and become inoperative at the end of the statement block they occur in. There are two exceptions to this rule:

  • The binding of the loop variable of a for loop is in scope for the body of the for loop, but not after the end of the loop.
  • All three portions of a repeat/until loop (the body, the test, and the fixup) act as a single scope, so symbols that are bound in the body are available in the test and the fixup.

For both types of loops, each pass through the loop runs in its own scope, so bindings from an earlier pass are not available in a later pass. For more information on these loops, see Control Flow.

Inner blocks inherit symbol bindings from outer blocks. You can only bind a symbol once per block; it is illegal to define a symbol with the same name as another symbol that is within scope (no "shadowing"). The following sequences are legal:

if (a == b) {
    ...
    let n = 5;
    ...             // n is 5
}
let n = 8;
...                 // n is 8

and

if (a == b) {
    ...
    let n = 5;
    ...             // n is 5
} else {
    ...
    let n = 8;
    ...             // n is 8
}
...                 // n is not bound to a value

But this would be illegal:

let n = 5;
...                 // n is 5
let n = 8;          // Error!!
...

as would:

let n = 8;
if (a == b) {
    ...             // n is 8
    let n = 5;      // Error!
    ...
}
...

Next steps

Learn about Working With Qubits in Q#.