Vorgänge und Funktionen

Wie in der Beschreibung des Qubit-Datentyps näher erläutert, werden Quantenberechnungen in Form von Nebenwirkungen von Vorgängen ausgeführt, die auf dem Zielquantenprozessor nativ unterstützt werden. Dies sind in der Tat die einzigen Nebenwirkungen in Q#. Da alle Typen unveränderlich sind, gibt es keine Nebenwirkungen, die einen Wert beeinflussen, der explizit in Q# dargestellt ist. Solange eine Implementierung einer bestimmten aufrufbaren Komponente weder direkt noch indirekt einen dieser nativ implementierten Vorgänge aufruft, erzeugt ihre Ausführung bei gleicher Eingabe immer die gleiche Ausgabe.

Q# ermöglicht es Ihnen, solche rein deterministischen Berechnungen explizit in Funktionen aufzuteilen. Da die nativ unterstützten Anweisungen nicht fest in die Sprache selbst integriert, sondern vollständig konfigurierbar und als Q#-Bibliothek ausgedrückt sind, wird der Determinismus dadurch garantiert, dass Funktionen nur andere Funktionen und keine Vorgänge aufrufen können. Darüber hinaus werden native Anweisungen, die nicht deterministisch sind, da sie den Quantenzustand beeinflussen, als Vorgänge dargestellt. Mit diesen beiden Einschränkungen können Funktionen ausgewertet werden, sobald ihr Eingabewert bekannt ist, und müssen im Prinzip nie mehr als einmal für die gleiche Eingabe ausgewertet werden.

Q# unterscheidet daher zwischen zwei Arten von aufrufbaren Komponenten: Vorgänge und Funktionen. Alle aufrufbaren Komponenten akzeptieren ein einzelnes (potenziell tupelwertiges) Argument als Eingabe und erzeugen einen einzelnen Wert (Tupel) als Ausgabe. Syntaktisch wird der Vorgangstyp als <TIn> => <TOut> is <Char> ausgedrückt, wobei <TIn> durch den Argumenttyp, <TOut> durch den Rückgabetyp und <Char> durch die Merkmale des Vorgangs zu ersetzen sind. Wenn keine Merkmale angegeben werden müssen, vereinfacht sich die Syntax zu <TIn> => <TOut>. Analog dazu werden Funktionstypen als <TIn> -> <TOut> ausgedrückt.

Abgesehen von dieser Determinismusgarantie gibt es kaum einen Unterschied zwischen Vorgängen und Funktionen. Beide sind Werte erster Klasse, die frei weitergegeben werden können. Sie können als Rückgabewerte oder Argumente für andere aufrufbare Komponenten verwendet werden, wie im folgenden Beispiel gezeigt:

function Pow<'T>(op : 'T => Unit, pow : Int) : 'T => Unit {
    return PowImpl(op, pow, _);
}

Beide können basierend auf einer typparametrisierten Definition instanziiert werden (wie etwa die typparametrisierte Funktion Pow weiter oben), und sie können teilweise angewendet werden, wie in der return-Anweisung des Beispiels.

Vorgangsmerkmale

Neben den Informationen über den Ein- und Ausgabetyp enthält der Vorgangstyp auch Informationen zu den Eigenschaften eines Vorgangs. Diese Informationen beschreiben zum Beispiel, welche Funktionselemente vom Vorgang unterstützt werden. Darüber hinaus enthält die interne Darstellung auch optimierungsrelevante Informationen, die vom Compiler abgeleitet werden.

Die Merkmale eines Vorgangs sind eine Reihe von vordefinierten und integrierten Bezeichnungen. Sie werden in Form eines speziellen Ausdrucks ausgedrückt, der Teil der Typsignatur ist. Der Ausdruck besteht entweder aus einer der vordefinierten Mengen von Bezeichnungen oder aus einer Kombination von Merkmalsausdrücken über einen unterstützten binären Operator.

Es gibt zwei vordefinierte Mengen, Adj undCtl.

  • Adj ist die Menge, die eine einzelne Bezeichnung enthält, durch die angegeben wird, dass ein Vorgang adjungierbar ist. Das bedeutet, er unterstützt das Funktionselement Adjoint, und die angewendete Quantentransformation kann rückgängig gemacht (invertiert) werden.
  • Ctlist die Menge, die eine einzelne Bezeichnung enthält, durch die angegeben wird, dass ein Vorgang steuerbar ist. Das bedeutet, er unterstützt das Funktionselement Controlled, und seine Ausführung kann vom Zustand anderer Qubits abhängig gemacht werden.

Die beiden Operatoren, die als Teil von Merkmalsausdrücken unterstützt werden, sind die Vereinigungsmenge + und die Schnittmenge *. In EBNF,

    predefined = "Adj" | "Ctl";
    characteristics = predefined 
        | "(", characteristics, ")" 
        | characteristics ("+"|"*") characteristics;

Wie zu erwarten, * hat eine höhere Rangfolge als +, und beide sind linksassoziativ. Der Typ eines unitären Vorgangs wird beispielsweise als <TIn> => <TOut> is Adj + Ctl ausgedrückt, wobei <TIn> durch den Typ des Vorgangsarguments und <TOut> durch den Typ des Rückgabewerts ersetzt werden muss.

Hinweis

Die Angabe der Merkmale eines Vorgangs in dieser Form hat zwei große Vorteile: Zum einen können neue Bezeichnungen eingeführt werden, ohne dass exponentiell viele Sprachschlüsselwörter für alle Kombinationen von Bezeichnungen benötigt werden. Noch wichtiger ist vielleicht, dass die Verwendung von Ausdrücken zur Angabe der Merkmale eines Vorgangs künftig auch Parametrisierungen über die Merkmale eines Vorgangs unterstützt.