Typparametrisierungen
Q# unterstützt typparametrisierte Vorgänge und Funktionen. Die Q#-Standardbibliotheken verwenden häufig typparametrisierte Aufrufe, um eine Vielzahl nützlicher Abstraktionen bereitzustellen, darunter Funktionen wie Mapped
und Fold
, die aus funktionalen Sprachen bekannt sind.
Um das Konzept der Typparametrisierung zu vermitteln, betrachten Sie das Beispiel der Funktion Mapped
, die eine bestimmte Funktion auf jeden Wert in einem Array anwendet und ein neues Array mit den berechneten Werten zurückgibt. Diese Funktionalität kann perfekt beschrieben werden, ohne die Elementtypen des Ein- und Ausgabearrays anzugeben. Da die genauen Typen die Implementierung der Funktion Mapped
nicht verändern, ist es sinnvoll, dass es möglich sein sollte, diese Implementierung für beliebige Elementtypen zu definieren. Dazu definieren wir eine Factory oder Vorlage, die bei Vorgabe der konkreten Typen für die Elemente im Ein- und Ausgabearray die entsprechende Funktionsimplementierung zurückgibt. Dieses Konzept wird in Form von Typparametern formalisiert.
Konkretisierung
Jeder Vorgang oder jede Funktionsdeklaration kann einen oder mehrere Typparameter angeben, die als Typen oder Teil der Typen der Eingabe und/oder Ausgabe der aufrufbaren Komponente verwendet werden können. Die Ausnahme sind Einstiegspunkte, die konkret sein müssen und nicht vom Typ parametrisiert werden können. Typparameternamen beginnen mit einem Apostroph (') und werden möglicherweise mehrmals in den Eingabe- und Ausgabetypen angezeigt. Alle Argumente, die dem gleichen Typparameter in der Aufrufsignatur entsprechen, müssen denselben Typ aufweisen.
Eine typparametrisierte aufrufbare Komponente muss konkretisiert werden, d. h. mit den notwendigen Typargumenten versehen werden, bevor sie zugewiesen oder als Argument übergeben werden kann, sodass alle Typparameter durch konkrete Typen ersetzt werden können. Ein Typ ist konkret, wenn er entweder einer der integrierten Typen oder ein benutzerdefinierter Typ ist oder wenn er im aktuellen Geltungsbereich konkret ist. Das folgende Beispiel veranschaulicht, was es bedeutet, wenn ein Typ im aktuellen Geltungsbereich konkret ist:
function Mapped<'T1, 'T2> (
mapper : 'T1 -> 'T2,
array : 'T1[]
) : 'T2[] {
mutable mapped = new 'T2[Length(array)];
for (i in IndexRange(array)) {
set mapped w/= i <- mapper(array[i]);
}
return mapped;
}
function AllCControlled<'T3> (
ops : ('T3 => Unit)[]
) : ((Bool,'T3) => Unit)[] {
return Mapped(CControlled<'T3>, ops);
}
Die Funktion CControlled
ist im Namespace Microsoft.Quantum.Canon
definiert. Sie nimmt einen Vorgang op
vom Typ 'TIn => Unit
als Argument und gibt einen neuen Vorgang vom Typ (Bool, 'TIn) => Unit
zurück, der den ursprünglichen Vorgang anwendet, sofern ein klassisches Bit (vom Typ Bool
) auf true gesetzt ist. Dies wird oft als die klassisch gesteuerte Version von op
bezeichnet.
Die Funktion Mapped
verwendet ein Array eines beliebigen Elementtyps 'T1
als Argument, wendet die angegebene Funktion mapper
auf jedes Element an und gibt ein neues Array vom Typ 'T2[]
zurück, das die zugeordneten Elemente enthält. Sie wird im Microsoft.Quantum.Array
-Namespace definiert. Für das Beispiel werden die Typparameter nummeriert, um die Diskussion nicht noch verwirrender zu machen, indem den Typparametern in beiden Funktionen derselbe Name gegeben wird. Dies ist nicht notwendig; Typparameter für verschiedene Aufrufe können denselben Namen haben, und der gewählte Name ist nur innerhalb der Definition dieses Aufrufs sichtbar und relevant.
Die Funktion AllCControlled
übernimmt ein Array von Vorgängen und gibt ein neues Array zurück, das die klassisch gesteuerten Versionen dieser Vorgänge enthält. Der Aufruf von Mapped
löst seinen Typparameter 'T1
in 'T3 => Unit
und seinen Typparameter 'T2
in (Bool,'T3) => Unit
auf. Die auflösenden Typargumente werden vom Compiler basierend auf dem Typ des angegebenen Arguments abgeleitet. Man sagt, dass sie implizit durch das Argument des Aufruf-Ausdrucks definiert sind. Typargumente können auch explizit festgelegt werden, wie es bei CControlled
in derselben Zeile geschieht. Die explizite Konkretisierung CControlled<'T3>
ist notwendig, wenn die Typargumente nicht hergeleitet werden können.
Der Typ 'T3
ist im Kontext von AllCControlled
konkret, da er bei jedem Aufruf von AllCControlled
bekannt ist. Das bedeutet Folgendes: Sobald der Einstiegspunkt des Programms (der nicht typparametrisiert werden kann) bekannt ist, ist auch der konkrete Typ 'T3
für jeden Aufruf von AllCControlled
bekannt, sodass eine geeignete Implementierung für diese spezielle Typauflösung generiert werden kann. Sobald der Einstiegspunkt eines Programms bekannt ist, können alle Verwendungen von Typparametern zur Kompilierzeit eliminiert werden. Man nennt diesen Prozess Monomorphisierung.
Es sind einige Einschränkungen erforderlich, um sicherzustellen, dass dies tatsächlich zur Kompilierzeit und nicht nur zur Laufzeit geschehen kann.
Beschränkungen
Betrachten Sie das folgenden Beispiel:
operation Foo<'TArg> (
op : 'TArg => Unit,
arg : 'TArg
) : Unit {
let cbit = RandomInt(2) == 0;
Foo(CControlled(op), (cbit, arg));
}
Abgesehen davon, dass ein Aufruf von Foo
zu einer Endlosschleife führt, dient es der Veranschaulichung.
Foo
ruft sich selbst mit der klassisch gesteuerten Version des ursprünglichen Vorgangs op
auf, der übergeben wurde, sowie mit einem Tupel, das zusätzlich zum ursprünglichen Argument ein zufälliges klassisches Bit enthält.
Für jede Iteration in der Rekursion wird der Typparameter 'TArg
des nächsten Aufrufs in (Bool, 'TArg)
aufgelöst, wobei 'TArg
der Typparameter des aktuellen Aufrufs ist. Nehmen wir an, dass Foo
mit dem Vorgang H
und einem Argument arg
vom Typ Qubit
aufgerufen wird. Dann ruft Foo
sich selbst mit einem Argument vom Typ (Bool, Qubit)
auf, das wiederum Foo
mit einem Argument vom Typ (Bool, (Bool, Qubit))
aufruft, und so weiter. In diesem Fall kann Foo
natürlich nicht zur Kompilierzeit monomorphisiert werden.
Zusätzliche Einschränkungen gelten für Zyklen im Aufrufdiagramm, die ausschließlich typparametrisierte aufrufbare Komponenten beinhalten. Jeder Aufruf muss nach dem Durchlaufen des Zyklus mit demselben Satz von Typargumenten aufgerufen werden.
Hinweis
Es ist möglich weniger restriktiv zu sein und zu verlangen, dass für jede aufrufbare Komponente im Zyklus eine begrenzte Anzahl von Zyklen vorhanden ist, nach denen er mit dem ursprünglichen Satz von Typargumenten aufgerufen wird, wie es bei der folgenden Funktion der Fall ist:
function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
Bar<'T2,'T3,'T1>(a2, a3, a1);
}
Der Einfachheit halber wird die restriktivere Anforderung erzwungen. Beachten Sie, dass bei Zyklen, die mindestens eine konkrete aufrufbare Komponente ohne Typparameter enthalten, eine solche aufrufbare Komponente sicherstellt, dass die typparametrisierten aufrufbaren Komponenten innerhalb dieses Zyklus immer mit einem festen Satz von Typargumenten aufgerufen werden.