Q

xiaoxiao2021-02-28  27

操作和功能 

Q#程序由一个或多个操作组成,这些操作描述量子操作对量子数据可能产生的副作用以及一个或多个允许修改经典数据的功能。 与操作相比,函数用于描述纯粹的经典行为,除了计算经典输出值之外,没有任何效果。

然后,Q#中定义的每个操作都可以调用任意数量的其他操作,包括由该语言定义的内置基本操作。 这些基本操作的具体定义取决于目标机器。 编译时,每个操作都表示为可以提供给目标计算机的.NET类类型。

定义新的操作

如上所述,用Q#编写的量子程序最基本的构建块是一个操作 ,它可以从传统的.NET应用程序(例如使用模拟器)或Q#中的其他操作调用。 每个操作接受一个输入,产生一个输出,并且最少由一个列出一个或多个指令的主体组成。 例如,以下操作将单个量子位作为其输入,然后在该输入上调用内置的X操作:

Q# operation BitFlip(target : Qubit) : () {     body {         X(target);     } }

关键字operation开始操作定义,后跟名称; 这里是BitFlip 。 接下来,输入的类型被定义为Qubit ,以及用于在新操作中引用输入的名称target 。 同样, ()定义操作的输出是空的。 这与C#和其他命令式语言中的void类似,相当于F#和其他函数式语言中的单元。

注意

我们将在下面更详细地探讨这一点,但Q#中的每个操作只需要一个输入并返回一个输出。 然后使用元组来表示多个输入和输出,这些元组将多个值一起收集到一个值中。 非正式地说,我们说Q#是一种“元组元组”语言。 遵循这个概念, ()应该被读作“空”元组。

在新操作中,关键字body用于声明组成新操作的语句顺序。 在上面的例子中,唯一的声明是调用内置于Q#前奏的X操作。

操作也可以返回比()更有趣的类型。 例如, M操作返回类型(Result)的输出,表示已经执行了测量。 我们可以将操作的输出传递给另一个操作,也可以将它与let关键字一起用于定义一个新变量。

这允许表示与量子运算在较低级别交互的经典计算,例如在超密码编码中:

Q# operation Superdense(here : Qubit, there : Qubit) : (Result, Result) {     body {         CNOT(there, here);         H(there);         let firstBit = M(there);         let secondBit = M(here);         return (firstBit, secondBit);     } }

如果一个操作没有返回除()以外的值,那么它也可以指定变体和正文,指定操作在被调整或控制时的操作方式。 操作的伴随变体指定其在反向运行时的行为方式,而受控变体指定当对条件应用于量子寄存器的状态时操作的行为。

注意

Q#中的许多操作表示单一门。 如果U

是由操作U表示的单一门,则(Adjoint U)表示单一门U dagger

在这两种情况下,变体规范都紧跟在主体定义的末尾:

Q# operation PrepareEntangledPair(here : Qubit, there : Qubit) : () {     body {         H(here);         CNOT(here, there);     }     adjoint auto     controlled auto     controlled adjoint auto }

通常,变体规范将包含关键字auto ,表示编译器应确定如何生成变体定义。 如果编译器不能自动生成定义,或者如果可以给出更高效的实现,则也可以手动定义变体。 我们将在高阶控制流程中看到下面的例子。

要调用操作的变体,请使用Adjoint或Controlled关键字。 例如,通过使用PrepareEntangledState的伴随将纠缠态转换回非缠结的一对量子位,上面的超级密码编码示例可以更紧凑地编写:

Q# operation Superdense(here : Qubit, there : Qubit) : (Result, Result) {     body {         (Adjoint PrepareEntangledPair)(there, here);         let firstBit = M(there);         let secondBit = M(here);         return (firstBit, secondBit);     } }

设计用于变体的操作时需要考虑许多重要限制。 最关键的是,使用任何其他操作的输出值的操作不能使用auto关键字来指定变体,因为在这样的操作中如何重新排序语句以获得相同的效果是不明确的。

定义新功能

Q#还允许定义不同于操作的函数 ,因为它们不允许在计算输出值之外产生任何影响。 特别是,函数不能调用操作,对量子位进行操作,对随机数采样或以其他方式依赖于输入值以外的状态。 因此,Q#函数是纯粹的 ,因为它们总是将相同的输入值映射到相同的输出值。 这允许Q#编译器在生成操作变体时安全地重新命名函数的调用方式和时间。

除了语句直接放在函数中,定义一个函数与定义一个操作类似,不需要包装在一个body声明中。 例如:

Q# function Square(x : Double) : (Double) {     return x * x; }

只要有可能这样做,根据功能而不是操作写出经典逻辑是有帮助的,以便在操作中更容易使用。 例如,如果我们将Square写为操作,那么编译器将无法保证使用相同的输入调用它将持续生成相同的输出。 考虑操作变体时,这一点尤为重要。

为了强调函数和操作之间的差异,请考虑从Q#操作中经典抽样随机数的问题:

Q# operation U(target : Qubit) : () {     body {         let angle = RandomReal()         Rz(angle, target)     } }

每次调用U ,它都会对target采取不同的操作。 尤其是,编译器不能保证如果我们向U添加了一个Adjoint auto语句,那么U(target); (Adjoint U)(target); U(target); (Adjoint U)(target); 作为身份(即,作为无操作)。 这违反了我们在Vectors和Matrices中看到的伴随的定义,例如允许在我们调用RandomReal操作的操作中使用Adjoint自动会破坏编译器提供的保证; RandomReal是不存在伴随和受控版本的操作。

另一方面,允许Square等函数调用是安全的,因为编译器可以确信只需要将输入保持为Square以保持其输出稳定。 因此,将尽可能多的经典逻辑分离到函数中可以很容易地在其他函数和操作中重用该逻辑。

控制流

在一个操作或函数中,每个语句按顺序执行,类似于大多数常见的命令式经典语言。 这种控制流程可以通过三种不同的方式进行修改:

if陈述 for循环 repeat - until循环

我们推迟讨论后者,直到我们讨论重复直至成功(RUS)电路。 然而, if和for控制流构造在大多数古典编程语言的熟悉意义上进行。 特别是, if语句可以接受一个条件,后面可以跟一个或多个elif语句,并且可以以else结束:

Q# if (pauli == PauliX) {     X(qubit); } elif (pauli == PauliY) {     Y(qubit); } elif (pauli == PauliZ) {     Z(qubit); } else {     fail "Cannot use PauliI here."; }

同样, for循环表示对整个范围的迭代:

Q# for (idxQubit in 0..nQubits - 1) { // Do something to idxQubit... }

重要的是, for循环甚至可以在声明adjoint auto变体的操作中使用,在这种情况下, for循环的伴随逆转方向并采用每次迭代的伴随。 这遵循“鞋袜”的原则:如果你想撤销穿上袜子和鞋子,你必须撤销穿上鞋子然后撤回穿上袜子。 当你还穿着你的鞋子时,试穿和脱下袜子显然不太合适!

作为一流价值的操作和功能

使用函数而不是操作来推理控制流和经典逻辑的关键技术是利用Q#中的操作和函数是一流的 。 也就是说,它们各自都是语言本身的价值观。 例如,如果有一点间接的话,以下是完全有效的Q#代码:

Q# operation FirstClassExample(target : Qubit) : () {     body {         let ourH = H;         ourH(target);     } }

上面代码片段中的变量ourH的值就是操作H ,这样我们可以像调用其他操作那样调用该值。 这允许我们编写将操作作为其输入的一部分的操作,形成更高阶的控制流概念。 例如,我们可以想象通过将它应用两次到相同的目标量子位来“操作”一个操作。

Q# operation ApplyTwice(op : ((Qubit) => ()), target : Qubit) : () {     body {         op(target);         op(target);     } }

在这个例子中,出现在类型((Qubit) => ())的=>箭头表示输入字段op是一个操作,它将输入的类型(Qubit)作为输入并生成一个空元组输出。 可选地,我们可以通过在输出类型之后指定变体来指定操作类型支持一种或两种变体,如((Qubit) => () : Adjoint) 。 当我们更普遍地讨论Q#中的类型时,我们将在下面进一步探讨。

但是现在我们强调,我们也可以将操作作为输出的一部分返回,这样我们就可以将某些经典的条件逻辑作为经典函数进行隔离,该函数以操作的形式返回量子程序的描述。 作为一个简单的例子,考虑传送示例,其中接收到两位古典消息的一方需要使用该消息来将它们的量子位解码为适当的传送状态。 我们可以用一个函数来写这个函数,该函数采用这两个经典位并返回正确的解码操作。

Q# function TeleporationDecoderForMessage(hereBit : Result, thereBit : Result)         : ((Qubit) => () : Adjoint, Controlled) {     if (hereBit == Zero && thereBit == Zero) {         return I;     } elif (hereBit == One && thereBit == Zero) {         return X;     } elif (hereBit == Zero && thereBit == One) {         return Z;     } else {         return Y;     } }

这个新函数确实是一个函数,因为如果我们用hereBit和thereBit的相同值调用它,我们总是会返回相同的操作。 因此,解码器可以在操作内部安全地运行,而无需推理解码逻辑如何与不同操作变体的定义交互。 也就是说,我们在函数内部隔离了经典逻辑,保证编译器只要输入被保留就可以重新排序函数调用而不受惩罚。

我们也可以把函数当作第一类值来对待,因为当我们讨论操作和函数类型时,我们会更详细地看到它们。

部分应用操作和功能

我们可以通过使用部分应用程序来返回操作的函数做更多的事情,在这些函数中我们可以提供一个或多个输入到一个函数或操作的部分,而不用实际调用它。 例如,回顾上面的ApplyTwice示例,我们可以指出我们不想指定输入操作应立即应用哪个量子位:

Q# operation PartialApplicationExample(op : ((Qubit) => ()), target : Qubit) : () {     body {         let twiceOp = ApplyTwice(op, _);         twiceOp(target);     } }

在这种情况下,局部变量twiceOp保存部分应用的操作ApplyTwice(op, _) ,其中尚未指定的部分输入用_表示。 当我们在下一行中实际调用twiceOp时,我们将作为输入传递给部分应用操作,将输入的所有其余部分传递给原始操作。 因此,上面的代码片段与直接调用ApplyTwice(op, target)效果完全相同, ApplyTwice(op, target) ,我们引入了一个新的局部变量,它允许我们在提供某些输入部分的同时延迟调用。

由于部分应用的操作在提供完整输入之前并未实际调用,因此即使在函数内部也可以部分应用操作。

Q# function SquareOperation(op : ((Qubit) => ())) : ((Qubit) => ()) {     return ApplyTwice(op, _); }

原则上, SquareOperation的经典逻辑可能涉及更多,但它仍然与操作的其余部分相隔离,因为编译器可以提供有关函数的保证。 这种方法将在整个Q#标准库中用于表达经典控制流程,以便在量子程序中使用。

转载请注明原文地址: https://www.6miu.com/read-2800227.html

最新回复(0)