操作和功能
Q#程序由一個或多個操作組成,這些操作描述量子操作對量子數據可能產生的副作用以及一個或多個允許修改經典數據的功能。 與操作相比,函數用於描述純粹的經典行爲,除了計算經典輸出值之外,沒有任何效果。
然後,Q#中定義的每個操作都可以調用任意數量的其他操作,包括由該語言定義的內置基本操作。 這些基本操作的具體定義取決於目標機器。 編譯時,每個操作都表示爲可以提供給目標計算機的.NET類類型。
定義新的操作
如上所述,用Q#編寫的量子程序最基本的構建塊是一個操作 ,它可以從傳統的.NET應用程序(例如使用模擬器)或Q#中的其他操作調用。 每個操作接受一個輸入,產生一個輸出,並且最少由一個列出一個或多個指令的主體組成。 例如,以下操作將單個量子位作爲其輸入,然後在該輸入上調用內置的X
操作:
operation BitFlip(target : Qubit) : () {
body {
X(target);
}
}
關鍵字operation
開始操作定義,後跟名稱; 這裏是BitFlip
。 接下來,輸入的類型被定義爲Qubit
,以及用於在新操作中引用輸入的名稱target
。 同樣, ()
定義操作的輸出是空的。 這與C#和其他命令式語言中的void
類似,相當於F#和其他函數式語言中的單元。
注意
我們將在下面更詳細地探討這一點,但Q#中的每個操作只需要一個輸入並返回一個輸出。 然後使用元組來表示多個輸入和輸出,這些元組將多個值一起收集到一個值中。 非正式地說,我們說Q#是一種“元組元組”語言。 遵循這個概念, ()
應該被讀作“空”元組。
在新操作中,關鍵字body
用於聲明組成新操作的語句順序。 在上面的例子中,唯一的聲明是調用內置於Q#前奏的X操作。
操作也可以返回比()
更有趣的類型。 例如, M操作返回類型(Result)
的輸出,表示已經執行了測量。 我們可以將操作的輸出傳遞給另一個操作,也可以將它與let
關鍵字一起用於定義一個新變量。
這允許表示與量子運算在較低級別交互的經典計算,例如在超密碼編碼中:
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。
在這兩種情況下,變體規範都緊跟在主體定義的末尾:
operation PrepareEntangledPair(here : Qubit, there : Qubit) : () {
body {
H(here);
CNOT(here, there);
}
adjoint auto
controlled auto
controlled adjoint auto
}
通常,變體規範將包含關鍵字auto
,表示編譯器應確定如何生成變體定義。 如果編譯器不能自動生成定義,或者如果可以給出更高效的實現,則也可以手動定義變體。 我們將在高階控制流程中看到下面的例子。
要調用操作的變體,請使用Adjoint
或Controlled
關鍵字。 例如,通過使用PrepareEntangledState
的伴隨將糾纏態轉換回非纏結的一對量子位,上面的超級密碼編碼示例可以更緊湊地編寫:
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
聲明中。 例如:
function Square(x : Double) : (Double) {
return x * x;
}
只要有可能這樣做,根據功能而不是操作寫出經典邏輯是有幫助的,以便在操作中更容易使用。 例如,如果我們將Square
寫爲操作,那麼編譯器將無法保證使用相同的輸入調用它將持續生成相同的輸出。 考慮操作變體時,這一點尤爲重要。
爲了強調函數和操作之間的差異,請考慮從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
結束:
if (pauli == PauliX) {
X(qubit);
} elif (pauli == PauliY) {
Y(qubit);
} elif (pauli == PauliZ) {
Z(qubit);
} else {
fail "Cannot use PauliI here.";
}
同樣, for
循環表示對整個範圍的迭代:
for (idxQubit in 0..nQubits - 1) { // Do something to idxQubit... }
重要的是, for
循環甚至可以在聲明adjoint auto
變體的操作中使用,在這種情況下, for
循環的伴隨逆轉方向並採用每次迭代的伴隨。 這遵循“鞋襪”的原則:如果你想撤銷穿上襪子和鞋子,你必須撤銷穿上鞋子然後撤回穿上襪子。 當你還穿着你的鞋子時,試穿和脫下襪子顯然不太合適!
作爲一流價值的操作和功能
使用函數而不是操作來推理控制流和經典邏輯的關鍵技術是利用Q#中的操作和函數是一流的 。 也就是說,它們各自都是語言本身的價值觀。 例如,如果有一點間接的話,以下是完全有效的Q#代碼:
operation FirstClassExample(target : Qubit) : () {
body {
let ourH = H;
ourH(target);
}
}
上面代碼片段中的變量ourH
的值就是操作H ,這樣我們可以像調用其他操作那樣調用該值。 這允許我們編寫將操作作爲其輸入的一部分的操作,形成更高階的控制流概念。 例如,我們可以想象通過將它應用兩次到相同的目標量子位來“操作”一個操作。
operation ApplyTwice(op : ((Qubit) => ()), target : Qubit) : () {
body {
op(target);
op(target);
}
}
在這個例子中,出現在類型((Qubit) => ())
的=>
箭頭表示輸入字段op
是一個操作,它將輸入的類型(Qubit)
作爲輸入並生成一個空元組輸出。 可選地,我們可以通過在輸出類型之後指定變體來指定操作類型支持一種或兩種變體,如((Qubit) => () : Adjoint)
。 當我們更普遍地討論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
示例,我們可以指出我們不想指定輸入操作應立即應用哪個量子位:
operation PartialApplicationExample(op : ((Qubit) => ()), target : Qubit) : () {
body {
let twiceOp = ApplyTwice(op, _);
twiceOp(target);
}
}
在這種情況下,局部變量twiceOp
保存部分應用的操作ApplyTwice(op, _)
,其中尚未指定的部分輸入用_
表示。 當我們在下一行中實際調用twiceOp
時,我們將作爲輸入傳遞給部分應用操作,將輸入的所有其餘部分傳遞給原始操作。 因此,上面的代碼片段與直接調用ApplyTwice(op, target)
效果完全相同, ApplyTwice(op, target)
,我們引入了一個新的局部變量,它允許我們在提供某些輸入部分的同時延遲調用。
由於部分應用的操作在提供完整輸入之前並未實際調用,因此即使在函數內部也可以部分應用操作。
function SquareOperation(op : ((Qubit) => ())) : ((Qubit) => ()) {
return ApplyTwice(op, _);
}
原則上, SquareOperation
的經典邏輯可能涉及更多,但它仍然與操作的其餘部分相隔離,因爲編譯器可以提供有關函數的保證。 這種方法將在整個Q#標準庫中用於表達經典控制流程,以便在量子程序中使用。