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 => Unitund 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.