microsoft / qsharp-language

Official repository for design of the quantum programming language Q# and its core libraries
MIT License
235 stars 56 forks source link

Qubit allocation without new block scope #14

Closed bamarsha closed 3 years ago

bamarsha commented 3 years ago

Suggestion

We propose creating an analogue of let for qubits: a statement that allocates qubits and binds them to a name for the remainder of the current scope, after which the name is no longer accessible and the qubits are released. The statement does not require a new block scope, unlike the current using and borrowing statements.

Considerations

Allocating qubits in Q# with using and borrowing statements requires creating a new block scope. The block makes the lifetime of the qubits explicit, and discourages holding on to qubits for longer than needed, but at the cost of increased block nesting and indentation. Additionally, it is very common for a using statement block to be the last statement in the enclosing scope, in which case the explicit scope did not actually help to shorten the lifetime of the allocated qubits.

The explicit block also makes it cumbersome to create more than one qubit variable at a time. If one using statement per variable is used, a new indentation level is required for each variable. More commonly, tuple destructuring is used to create more than one variable in a single assignment, but this becomes difficult to read as the number of variables increases or as the complexity of the qubit initializer expression increases.

A let-like qubit allocation statement is needed to simplify these use cases. Because using and borrowing statements are the only way to allocate qubits, there is no workaround currently in Q#. An alternative proposal that would only address the many variable case could be to allow multiple using statements in a row followed by only one scope:

using (q1 = Qubit())
using (q2 = Qubit()) {
    // ... use both q1 and q2 ...
}

But this still requires at least one new block scope in all cases, unlike the original suggestion.

Context

We can consider two possible naming conventions for the new statements:

  1. Reuse the same keywords as the block statements: using and borrowing.
  2. By analogy to let, create the new keywords use and borrow.

Reusing the keywords avoids a breaking change, but creates a slight inconsistency:

using q1 = Qubit();
borrowing q2 = Qubit();
letting q3 = q1; // wrong
let q3 = q1;     // right

If we use new keywords, we need to decide if we keep the original keywords for the block statements (supporting two parallel sets of keywords indefinitely), or if we rename those too:

use (q = Qubit()) {
    // ...
}
borrow (q = Qubit()) {
    // ...
}

Examples

Example 1: Current syntax.

// One variable.
using (q = Qubit()) {
    // ...
}

// Two variables with separate statements.
using (q1 = Qubit()) {
    using (q2 = Qubit()) {
        // ...
    }
}

// Two variables with tuple destructuring.
using ((q1, q2) = (Qubit(), Qubit())) {
    // ...
}

Example 2: Proposed syntax with new keywords.

// One variable.
use q = Qubit();
// ...

// Two variables with separate statements.
use q1 = Qubit();
use q2 = Qubit();
// ...

// Two variables with tuple destructuring.
use (q1, q2) = (Qubit(), Qubit());
// ...

Affidavit (please fill out)

Please add ticks by placing a cross in the box:

Please tick all that apply:

alan-geller commented 3 years ago

One thought that is perhaps slightly off-topic but seems intimately related: why is it Qubit() in a using (or now use) statement, rather than simply Qubit? What function do the parentheses play? We're not calling a constructor. The array form doesn't take parens. I think there would be some readability benefit from making the statement simply:

use q = Qubit;
bamarsha commented 3 years ago

I actually do sort of think of Qubit() as being a type constructor, just a non-deterministic one that can only be called within a using statement. For qubit arrays, they remind me of new array expressions without the new - maybe it should actually be new Qubit[n] in the current syntax (but there's no replacement if/when new is deprecated, since ConstantArray needs an initial value).

alan-geller commented 3 years ago

I actually do sort of think of Qubit() as being a type constructor, just a non-deterministic one that can only be called within a using statement. For qubit arrays, they remind me of new array expressions without the new - maybe it should actually be new Qubit[n] in the current syntax (but there's no replacement if/when new is deprecated, since ConstantArray needs an initial value).

Hmm... I think I'd either go with () in both cases or neither. Right now it feels inconsistent to me.

bamarsha commented 3 years ago

With the ()-less version, it feels to me either like there is only one qubit globally (similar to a constant like One or PauliZ), or that it's an unapplied callable of type Unit => Qubit. I can't think of any other context where a ()-less expression has side effects.

bettinaheim commented 3 years ago

I haven't looked through the full proposal yet (sorry), but just as a thought seeing this conversation: Reexamining exactly how initializers should look like makes sense to me. Two related items we should consider then are 1.) Whether it makes sense to have a convenient way (syntax) for initializing qubits with a particular operation upon allocation, and how that would look like. 2.) How allocations would look like if we were to support allocating a 2D grid of manually layed out qubits.

bettinaheim commented 3 years ago

See PR https://github.com/microsoft/qsharp-language/pull/33 for the proposal and additional discussions.