Type parameteriseringen
Q# ondersteunt bewerkingen en functies met typeparameter. De Q# standaardbibliotheken maken intensief gebruik van type-geparametriseerde aanroepbare aanroepfuncties om een groot aantal nuttige abstracties te bieden, waaronder functies zoals Mapped
en Fold
die bekend zijn uit functionele talen.
Als u het concept van typeparametrisaties wilt motiveren, bekijkt u het voorbeeld van de functie Mapped
, die een bepaalde functie toepast op elke waarde in een matrix en een nieuwe matrix retourneert met de berekende waarden. Deze functionaliteit kan perfect worden beschreven zonder de itemtypen van de invoer- en uitvoermatrices op te geven. Aangezien de exacte typen de implementatie van de functie Mapped
niet wijzigen, is het logisch dat deze implementatie kan worden gedefinieerd voor willekeurige itemtypen. We willen een factory of sjabloon definiëren die, gezien de concrete typen voor de items in de invoer- en uitvoermatrix, de bijbehorende functie-implementatie retourneert. Dit idee wordt geformaliseerd in de vorm van typeparameters.
Concretisatie
Elke bewerking of functiedeclaratie kan een of meer typeparameters opgeven die kunnen worden gebruikt als de typen, of een deel van de typen, van de invoer of uitvoer van het aanroepbare, of beide. De uitzonderingen zijn toegangspunten, die beton moeten zijn en niet kunnen worden geparametriseerd. Typeparameternamen beginnen met een vinkje (') en kunnen meerdere keren worden weergegeven in de invoer- en uitvoertypen. Alle argumenten die overeenkomen met dezelfde typeparameter in de aanroepbare handtekening moeten van hetzelfde type zijn.
Een type-geparametriseerde aanroepbare moet worden geconcretiseerd, dat wil gezegd dat het moet worden voorzien van de benodigde typeargumenten voordat deze kan worden toegewezen of doorgegeven als argument, zodat alle typeparameters kunnen worden vervangen door concrete typen. Een type wordt beschouwd als beton als het een van de ingebouwde typen is, een door de gebruiker gedefinieerd type of als het beton binnen het huidige bereik is. In het volgende voorbeeld ziet u wat het betekent dat een type concreet is binnen het huidige bereik. Hieronder wordt meer in detail uitgelegd:
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);
}
De functie CControlled
wordt gedefinieerd in de Microsoft.Quantum.Canon
naamruimte. Het gebruikt een bewerking op
van het type 'TIn => Unit
als argument en retourneert een nieuwe bewerking van het type (Bool, 'TIn) => Unit
die de oorspronkelijke bewerking toepast, mits een klassieke bit (van het type Bool
) is ingesteld op true. Dit wordt vaak de klassiek beheerde versie van op
genoemd.
De functie Mapped
gebruikt een matrix van een willekeurig itemtype 'T1
als argument, past de opgegeven mapper
functie toe op elk item en retourneert een nieuwe matrix van het type 'T2[]
die de toegewezen items bevat. Deze wordt gedefinieerd in de Microsoft.Quantum.Array
naamruimte. Voor het doel van het voorbeeld worden de typeparameters genummerd om te voorkomen dat de discussie verwarrender wordt door de typeparameters in beide functies dezelfde naam te geven. Dit is niet nodig; typeparameters voor verschillende aanroepbare functies kunnen dezelfde naam hebben en de gekozen naam is alleen zichtbaar en relevant binnen de definitie van dat aanroepbare.
De functie AllCControlled
neemt een matrix van bewerkingen en retourneert een nieuwe matrix met de klassiek beheerde versies van deze bewerkingen. Met de aanroep van Mapped
wordt de typeparameter 'T1
omgezet in 'T3 => Unit
, en de parameter 'T2
van het type naar (Bool,'T3) => Unit
. De argumenten voor het omzetten van het type worden afgeleid door de compiler op basis van het type van het opgegeven argument. We zeggen dat ze impliciet worden gedefinieerd door het argument van de aanroepexpressie. Typeargumenten kunnen ook expliciet worden opgegeven, zoals in dezelfde regel wordt gedaan CControlled
. De expliciete concretisatie CControlled<'T3>
is nodig wanneer de typeargumenten niet kunnen worden afgeleid.
Het type 'T3
is concreet binnen de context van AllCControlled
, omdat het bekend is voor elke aanroep van AllCControlled
. Dat betekent dat zodra het ingangspunt van het programma - dat niet kan worden geparametriseerd - bekend is, net als het concrete type 'T3
voor elke aanroep naar AllCControlled
, zodat een geschikte implementatie voor dat specifieke type resolutie kan worden gegenereerd. Zodra het beginpunt van een programma bekend is, kunnen alle gebruik van type parameters tijdens het compileren worden geëlimineerd. We noemen dit proces monomorfisatie.
Er zijn enkele beperkingen nodig om ervoor te zorgen dat dit inderdaad kan worden gedaan tijdens het compileren in plaats van alleen tijdens runtime.
Beperkingen
Kijk eens naar het volgende voorbeeld:
operation Foo<'TArg> (
op : 'TArg => Unit,
arg : 'TArg
) : Unit {
let cbit = RandomInt(2) == 0;
Foo(CControlled(op), (cbit, arg));
}
Negerend dat een aanroep van Foo
zal resulteren in een oneindige lus, dient het ter illustratie.
Foo
roept zichzelf aan met de klassiek beheerde versie van de oorspronkelijke bewerking op
die is doorgegeven, evenals een tuple met een willekeurige klassieke bit naast het oorspronkelijke argument.
Voor elke iteratie in de recursie wordt het typeparameter 'TArg
van de volgende aanroep omgezet in (Bool, 'TArg)
, waarbij 'TArg
de typeparameter van de huidige aanroep is. Stel concreet dat Foo
wordt aangeroepen met de bewerking H
en een argument arg
van het type Qubit
.
Foo
roept zichzelf vervolgens aan met een typeargument (Bool, Qubit)
, dat vervolgens wordt aangeroepen Foo
met een typeargument (Bool, (Bool, Qubit))
, enzovoort. Het is duidelijk dat in dit geval Foo
niet kan worden monomorfiseerd tijdens het compileren.
Aanvullende beperkingen zijn van toepassing op cycli in de aanroepgrafiek die alleen betrekking hebben op type-geparametriseerde aanroepbare apparaten. Elke aanroepbare moet worden aangeroepen met dezelfde set typeargumenten nadat de cyclus is doorlopen.
Notitie
Het zou mogelijk zijn om minder beperkend te zijn en te vereisen dat voor elke aanroepbare in de cyclus een eindig aantal cycli is waarna deze wordt aangeroepen met de oorspronkelijke set typeargumenten, zoals het geval is voor de volgende functie:
function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
Bar<'T2,'T3,'T1>(a2, a3, a1);
}
Voor het gemak wordt de meer beperkende vereiste afgedwongen. Houd er rekening mee dat voor cycli die betrekking hebben op ten minste één concrete aanroepbare zonder typeparameter, een dergelijke aanroepbare ervoor zorgt dat de type-geparametriseerde aanroepbare aanroepbare elementen binnen die cyclus altijd worden aangeroepen met een vaste set typeargumenten.