Parametrizace typů
Q# podporuje typ parametrizované operace a funkce. Standardní Q# knihovny velmi využívají typ-parametrizované volatables k poskytování řadu užitečných abstrakcí, včetně funkcí jako Mapped
a Fold
, které jsou známé z funkčních jazyků.
Pokud chcete motivovat koncept parametrizace typů, podívejte se na příklad funkce Mapped
, která použije danou funkci na každou hodnotu v poli a vrátí nové pole s vypočítanými hodnotami. Tuto funkci lze dokonale popsat bez zadání typů položek vstupních a výstupních polí. Vzhledem k tomu, že přesné typy nemění implementaci funkce Mapped
, dává smysl, že by mělo být možné definovat tuto implementaci pro typy libovolných položek. Chceme definovat továrnu nebo šablonu , která vzhledem k konkrétním typům pro položky ve vstupním a výstupním poli vrátí odpovídající implementaci funkce. Tento pojem je formalizován ve formě parametrů typu.
Konkretizace
Jakákoli operace nebo deklarace funkce může určit jeden nebo více parametrů typu, které lze použít jako typy nebo část typů, vstupu nebo výstupu volatelného vstupu nebo výstupu, případně obojího. Výjimkou jsou vstupní body, které musí být konkrétní a nelze je typově parametrizovat. Názvy parametrů typu začínají zaškrtnutím (') a ve vstupním a výstupním typu se můžou objevit několikrát. Všechny argumenty, které odpovídají stejnému parametru typu v volatelném podpisu, musí být stejného typu.
Parametrizovaný volatelný typ musí být konkretizován, to znamená, že musí být opatřen potřebnými argumenty typu, aby bylo možné jej přiřadit nebo předat jako argument, aby všechny parametry typu mohly být nahrazeny konkrétními typy. Typ se považuje za beton, pokud je to jeden z předdefinovaných typů, typ definovaný uživatelem nebo pokud je beton v rámci aktuálního rozsahu. Následující příklad ukazuje, co znamená, že typ má být konkrétní v rámci aktuálního oboru, a je podrobněji vysvětlen níže:
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);
}
Funkce CControlled
je definována Microsoft.Quantum.Canon
v oboru názvů. Přijímá operaci op
typu 'TIn => Unit
jako argument a vrací novou operaci typu (Bool, 'TIn) => Unit
, která použije původní operaci, za předpokladu, že klasický bit (typu Bool
) je nastaven na hodnotu true; tato operace se často označuje jako klasicky řízená verze nástroje op
.
Funkce Mapped
přebírá pole libovolného typu 'T1
položky jako argument, použije danou mapper
funkci na každou položku a vrátí nové pole typu 'T2[]
obsahující namapované položky. Je definován v Microsoft.Quantum.Array
oboru názvů. Pro účely tohoto příkladu jsou parametry typu očíslovány, aby diskuze byla matoucí, protože parametry typu v obou funkcích mají stejný název. To není nutné; Parametry typu pro různé volatelné můžou mít stejný název a zvolený název je viditelný a relevantní pouze v rámci definice tohoto volatelného.
Funkce AllCControlled
přebírá pole operací a vrací nové pole obsahující klasicky řízené verze těchto operací. Volání Mapped
přeloží parametr 'T1
typu na 'T3 => Unit
a jeho parametr 'T2
typu na (Bool,'T3) => Unit
. Argumenty typu překladu jsou odvozeny kompilátorem na základě typu daného argumentu. Říkáme, že jsou implicitně definovány argumentem výrazu volání. Argumenty typu lze také zadat explicitně, jak je tomu u CControlled
stejného řádku. Explicitní konkretizace CControlled<'T3>
je nutná, pokud argumenty typu nelze odvodit.
Typ 'T3
je konkrétní v kontextu AllCControlled
, protože je známý pro každé vyvoláníAllCControlled
. To znamená, že jakmile je znám vstupní bod programu - který nelze typ-parametrizovat - tak je konkrétní typ 'T3
pro každé volání AllCControlled
, aby bylo možné vygenerovat vhodnou implementaci pro tento konkrétní typ rozlišení. Jakmile je vstupní bod do programu známý, lze při kompilaci eliminovat všechna použití parametrů typu. Tento proces označujeme jako monomorfizaci.
Některá omezení jsou nutná, aby se zajistilo, že to skutečně lze provést v době kompilace, nikoli pouze za běhu.
Omezení
Uvažujte následující příklad:
operation Foo<'TArg> (
op : 'TArg => Unit,
arg : 'TArg
) : Unit {
let cbit = RandomInt(2) == 0;
Foo(CControlled(op), (cbit, arg));
}
Když ignorujeme, že vyvolání Foo
bude mít za následek nekonečnou smyčku, slouží pro ilustraci.
Foo
vyvolá sám sebe s klasicky řízenou verzí původní operace op
, která byla předána, stejně jako řazená kolekce členů obsahující náhodný klasický bit kromě původního argumentu.
Pro každou iteraci v rekurzi se parametr 'TArg
typu dalšího volání přeloží na (Bool, 'TArg)
, kde 'TArg
je parametr typu aktuálního volání. Konkrétně předpokládejme, že Foo
je vyvolána s operací H
a argumentem arg
typu Qubit
.
Foo
pak se vyvolá pomocí argumentu (Bool, Qubit)
typu , který se pak vyvolá Foo
s argumentem (Bool, (Bool, Qubit))
typu atd. Je zřejmé, že v tomto případě Foo
nelze monomorfizovat v době kompilace.
Další omezení platí pro cykly v grafu volání, které zahrnují pouze typově parametrizované volatelné položky. Každé volatelné volání musí být vyvoláno se stejnou sadou argumentů typu po procházení cyklu.
Poznámka
Bylo by možné být méně omezující a vyžadovat, aby pro každý volatelný v cyklu existoval konečný počet cyklů, po kterém se vyvolá s původní sadou argumentů typu, například případ pro následující funkci:
function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
Bar<'T2,'T3,'T1>(a2, a3, a1);
}
Pro zjednodušení se vynucuje restriktivnější požadavek. Všimněte si, že u cyklů, které zahrnují alespoň jeden konkrétní volatelný bez parametru typu, takové volatelné zajistí, že typově parametrizované volání v daném cyklu budou vždy volány s pevnou sadou argumentů typu.