Parametryzacje typów
Q# obsługuje operacje i funkcje sparametryzowane typu. Biblioteki Q# standardowe wykorzystują parametryzowane wywołania typu w celu zapewnienia wielu przydatnych abstrakcji, w tym funkcji, takich jak Mapped
i Fold
znanych z języków funkcjonalnych.
Aby zmotywować koncepcję parametryzacji typów, rozważ przykład funkcji Mapped
, która stosuje daną funkcję do każdej wartości w tablicy i zwraca nową tablicę z obliczonymi wartościami. Ta funkcja może być doskonale opisana bez określania typów elementów tablic wejściowych i wyjściowych. Ponieważ dokładne typy nie zmieniają implementacji funkcji Mapped
, warto zdefiniować tę implementację dla dowolnych typów elementów. Chcemy zdefiniować fabrykę lub szablon , który, biorąc pod uwagę konkretne typy elementów w tablicy wejściowej i wyjściowej, zwraca odpowiednią implementację funkcji. To pojęcie jest sformalizowane w postaci parametrów typu.
Concretization
Każda operacja lub deklaracja funkcji może określać co najmniej jeden parametr typu, który może być używany jako typy lub część typów, danych wejściowych lub wyjściowych obiektu wywołującego albo obu tych typów. Wyjątki to punkty wejścia, które muszą być betonowe i nie mogą być parametryzowane przez typ. Nazwy parametrów typu zaczynają się od znacznika (') i mogą pojawiać się wiele razy w typach wejściowych i wyjściowych. Wszystkie argumenty odpowiadające temu samemu parametrowi typu w podpisie wywołującym muszą być tego samego typu.
Wywołanie parametryzowane typu musi zostać skomplowane, czyli musi być dostarczone z wymaganymi argumentami typu, zanim będzie można przypisać lub przekazać jako argument, tak aby wszystkie parametry typu mogły zostać zastąpione konkretnymi typami. Typ jest uważany za konkretny, jeśli jest jednym z wbudowanych typów, typu zdefiniowanego przez użytkownika lub jeśli jest określony w bieżącym zakresie. W poniższym przykładzie pokazano, co to oznacza, że typ ma być konkretny w bieżącym zakresie i wyjaśniono bardziej szczegółowo poniżej:
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);
}
Funkcja CControlled
jest definiowana Microsoft.Quantum.Canon
w przestrzeni nazw. Przyjmuje operację op
typu 'TIn => Unit
jako argument i zwraca nową operację typu(Bool, 'TIn) => Unit
, która stosuje oryginalną operację, pod warunkiem, że bit klasyczny (typu Bool
) jest ustawiony na wartość true. Jest to często nazywane klasycznie kontrolowaną wersją .op
Funkcja Mapped
przyjmuje tablicę dowolnego typu 'T1
elementu jako argument, stosuje daną mapper
funkcję do każdego elementu i zwraca nową tablicę typu 'T2[]
zawierającego zamapowane elementy. Jest on zdefiniowany w Microsoft.Quantum.Array
przestrzeni nazw. W tym przykładzie parametry typu są numerowane, aby uniknąć bardziej mylącej dyskusji, podając parametry typu w obu funkcjach o tej samej nazwie. Nie jest to konieczne; parametry typu dla różnych elementów wywołujących mogą mieć taką samą nazwę, a wybrana nazwa jest widoczna tylko i odpowiednia w definicji tego wywołania.
Funkcja AllCControlled
przyjmuje tablicę operacji i zwraca nową tablicę zawierającą klasycznie kontrolowane wersje tych operacji. Wywołanie metody Mapped
rozpoznaje parametr 'T1
typu na 'T3 => Unit
, a parametr 'T2
typu na (Bool,'T3) => Unit
. Argumenty typu rozpoznawania są wnioskowane przez kompilator na podstawie typu danego argumentu. Mówimy, że są one niejawnie zdefiniowane przez argument wyrażenia wywołania. Argumenty typów można również jawnie określić, tak jak to ma miejsce CControlled
w tym samym wierszu. Jawna concretyzacja CControlled<'T3>
jest niezbędna, gdy nie można wywnioskować argumentów typu.
Typ 'T3
jest konkretny w kontekście AllCControlled
, ponieważ jest znany dla każdego wywołaniaAllCControlled
. Oznacza to, że gdy tylko punkt wejścia programu - który nie może być typ-parametryzowany - jest znany, więc jest konkretny typ 'T3
dla każdego wywołania AllCControlled
metody , tak aby można było wygenerować odpowiednią implementację dla tego konkretnego typu rozpoznawania. Gdy punkt wejścia do programu jest znany, wszystkie użycie parametrów typu można wyeliminować w czasie kompilacji. Ten proces nazywamy monomorfizacją.
Konieczne są pewne ograniczenia w celu zapewnienia, że można to zrobić w czasie kompilacji, a nie tylko w czasie wykonywania.
Ograniczenia
Rozpatrzmy następujący przykład:
operation Foo<'TArg> (
op : 'TArg => Unit,
arg : 'TArg
) : Unit {
let cbit = RandomInt(2) == 0;
Foo(CControlled(op), (cbit, arg));
}
Ignorując, że wywołanie Foo
elementu spowoduje nieskończoną pętlę, służy do celów ilustracji.
Foo
wywołuje się przy klasycznej kontrolowanej wersji oryginalnej operacji op
, która została przekazana, a także krotka zawierająca losowy bit klasyczny oprócz oryginalnego argumentu.
Dla każdej iteracji w rekursji parametr typu następnego wywołania jest rozpoznawany jako (Bool, 'TArg)
, gdzie 'TArg
jest parametrem 'TArg
typu bieżącego wywołania. Załóżmy, że Foo
jest wywoływany z operacją H
i argumentem arg
typu Qubit
.
Foo
następnie wywoła sam argument (Bool, Qubit)
typu , który następnie wywoła Foo
argument typu z argumentem (Bool, (Bool, Qubit))
typu itd. Oczywiście w tym przypadku Foo
nie można monomorfizować w czasie kompilacji.
Dodatkowe ograniczenia dotyczą cykli na grafie wywołań, które obejmują tylko parametryzowane wywołania typu. Każde wywołanie musi być wywoływane przy użyciu tego samego zestawu argumentów typu po przejściu przez cykl.
Uwaga
Możliwe byłoby mniej restrykcyjne i wymagałoby, aby dla każdego wywołania w cyklu istniała skończona liczba cykli, po których jest wywoływany z oryginalnym zestawem argumentów typu, na przykład w przypadku następującej funkcji:
function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
Bar<'T2,'T3,'T1>(a2, a3, a1);
}
Dla uproszczenia jest wymuszane bardziej restrykcyjne wymaganie. Należy pamiętać, że w przypadku cykli, które wymagają co najmniej jednego konkretnego wywołania bez żadnego parametru typu, takie wywołanie zapewni, że parametryzowane wywołania typu w ramach tego cyklu są zawsze wywoływane ze stałym zestawem argumentów typu.