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

Allow `Qubit[n]` and `Qubit()` expressions in `within` blocks. #53

Open Strilanc opened 3 years ago

Strilanc commented 3 years ago

Currently, when you want to allocate several registers, it is quite cumbersome. You get a lot of indentation, you have to assign temporary names to the raw register before wrapping it into the type you actually want, etc.

One way to work around this issue would be if within blocks allowed arbitrary allocation expressions. That way there's no need for the temporary unwrapped names, and much less increase in indentation.

This feels like a natural thing to just allow inside the within block.

Examples

within {
    let a = LittleEndian(Qubit[10]);
    let b = LittleEndian(Qubit[10]);
    let c = LittleEndian(Qubit[10]);
} apply {
    ... code using a, b, c ...
}

Affidavit (please fill out)

Please add ticks by placing a cross in the box:

Please tick all that apply:

bamarsha commented 3 years ago

Hi, thanks for the suggestion! Could you clarify the relationship between this suggestion and #40 or #14? Specifically, would syntax like this work for you, or is the within block needed for another purpose?

use a = LittleEndian(Qubit[10]);
use b = LittleEndian(Qubit[10]);
use c = LittleEndian(Qubit[10]);
// ... code using a, b, c ...

The inline use statements are from #14, and some form of UDT-wrapping-at-allocation will be possible with #40, although the syntax isn't final yet.

Strilanc commented 3 years ago

Yes that would work. I'm saying that in the context of a within block the use is technically not necessary, because the scoping is automatic.

Will I also be able to write expressions like use padded_array = existing_array + Qubit[10]?

bamarsha commented 3 years ago

I haven't heard of plans to support that. It looks like it could be useful, but it may be confusing that some qubits in padded_array are released when padded_array goes out of scope, while other qubits (the ones in existing_array) are not. @bettinaheim, since you're working on qubit initializers, what do you think?

bettinaheim commented 3 years ago

I was hoping to give a more concrete reply with a suggested syntax, but since progress is a bit slower than I was hoping, let me reply regardless. @Strilanc I am assuming the + in your expression above is a concatenation, such that the entire statement is an array allocation followed by an array concatenation and assignment to padded_array. In that case, we are really only talking about one line of code that is saved here, so out of pure curiosity: Is that a big pain point e.g. because it is a very common pattern for you? Supporting something like this could naturally be covered as part of the initializer generalization I am working on. It hadn't occurred to me that there is a use case for supporting elevating functions to be initializers, but this example here is one. I need a bit more time to come up with a concrete suggestion for how exactly I think the concrete syntax could look like, and I appreciate any input on it.

Strilanc commented 3 years ago

Supporting something like this could naturally be covered as part of the initializer generalization I am working on

I'm unclear why it would interact with initializers. My intention is that this line of code:

let a = existing_array + [Qubit()]

Should be exactly identical to these two lines:

use __compiler_temporary_1 = Qubit()
let a = existing_array + [__compiler_temporary_1]
bamarsha commented 3 years ago

There's a convention built into the language that initializers like Qubit() can only be called from within a using or borrowing statement. This is to make it clear that when the variable declared by using or borrowing goes out of scope, the qubits allocated in the variable are released.

It would be possible to lift this restriction, but I'm not sure if we should. For example, in

let a = existing_array + [Qubit()];

the qubit allocated by Qubit() is still released when a goes out of scope, but this is easier to miss than if it was declared with

use a = existing + [Qubit()];

since now it's explicit that code for releasing qubits is automatically triggered when a goes out of scope. That leads to wanting to allow + to be used with a use statement, which would mean qubit initializers would need to support classical functions and operators.

Strilanc commented 3 years ago

I don't think it should be associated with a, since some of the other qubits should last longer. Maybe what I'm asking for is a "use expression":

let a = other + [use Qubit()]

bamarsha commented 3 years ago

I don't think it should be associated with a, since some of the other qubits should last longer.

I agree, I think this is a good reason not to allow that construct. use expressions were also considered recently in #33. Here's a (rather long) discussion about them: https://github.com/microsoft/qsharp-language/pull/33#discussion_r496271018

I think the main point from that discussion is that attaching use to the Qubit() instead of the variable name sort of obscures the lifetime of the qubit. Which scope is it bound to? In let a = other + [use Qubit()], it's bound to a (or to a temporary variable that has the same scope as a), but what about a call expression like MyOperation(use Qubit()). Is the lifetime bound to the scope where MyOperation is called from, or is it bound to the parameter of MyOperation? It seems like it is less intuitive to me than an explicit use q = Qubit(); statement.