Tworzenie podtypów i wariancja
Q# obsługuje tylko kilka mechanizmów konwersji. Niejawne konwersje mogą wystąpić tylko podczas stosowania operatorów binarnych, obliczania wyrażeń warunkowych lub konstruowania literału tablicy. W takich przypadkach określa się wspólny supertyp, a niezbędne konwersje są wykonywane automatycznie. Oprócz takich niejawnych konwersji jawne konwersje za pośrednictwem wywołań funkcji są możliwe i często konieczne.
Obecnie jedyną relacją podtypowania, która istnieje, ma zastosowanie do operacji. Intuicyjnie warto zastąpić operację, która obsługuje więcej niż wymagany zestaw elementów functor. W przypadku dwóch typów TIn
i TOut
relacja podtypowania jest
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
gdzie A :> B
wskazuje, że B
jest podtypem A
. Frazowane inaczej, B
jest bardziej restrykcyjne niż A
takie, że wartość typu B
może być używana wszędzie tam, gdzie wymagana jest wartość typu A
. Jeśli obiekt wywołujący opiera się na argumencie (elemencie) typu A
, argument typu B
można bezpiecznie zastąpić, ponieważ jeśli zapewnia wszystkie niezbędne możliwości.
Ten rodzaj polimorfizmu rozciąga się na krotki w tym, że krotka typu B
jest podtypem typu krotki A
, jeśli zawiera taką samą liczbę elementów, a typ każdego elementu jest podtypem odpowiedniego typu elementu w A
. Jest to nazywane podtypowaniem głębokości . Obecnie nie ma obsługi podtypowania szerokości, czyli nie ma relacji podtypu między dwoma typami struct
lub typem struct
i dowolnym typem wbudowanym. Istnienie operatora unwrap
, który umożliwia wyodrębnianie krotki zawierającej wszystkie nazwane elementy, zapobiega temu.
Uwaga
Jeśli obiekt wywołujący przetwarza argument typu A
, jest również w stanie przetworzyć argument typu B
. Jeśli element wywołujący jest przekazywany jako argument do innego elementu wywołującego, musi być w stanie przetwarzać wszystkie elementy, których może wymagać podpis typu. Oznacza to, że jeśli obiekt wywołujący musi być w stanie przetworzyć argument typu B
, każde wywołanie, które może przetworzyć bardziej ogólny argument typu A
można bezpiecznie przekazać. Z drugiej strony oczekujemy, że jeśli wymagamy, aby przekazane wywołanie zwracało wartość typu A
, obietnica zwrócenia wartości typu B
jest wystarczająca, ponieważ ta wartość zapewni wszystkie niezbędne możliwości.
Typ operacji lub funkcji jest kontrawariantny w typie argumentu i kowariantny w zwracanym typie.
A :> B
z tego względu oznacza, że w przypadku dowolnego typu T1
,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
gdzie →
tutaj może oznaczać funkcję lub operację i pomijamy wszelkie adnotacje dla cech.
Podstawianie A
odpowiednio (B → T2)
i (T2 → A)
oraz zastępowanie B
odpowiednio (A → T2)
i (T2 → B)
prowadzi do wniosku, że dla dowolnego konkretnego typu T2
,
((A → T2) → T1) :> ((B → T2) → T1), and
((T2 → B) → T1) :> ((T2 → A) → T1), and
(T1 → (B → T2)) :> (T1 → (A → T2)), and
(T1 → (T2 → A)) :> (T1 → (T2 → B))
Indukując, następuje, że każda dodatkowa pośrednia odwraca wariancję typu argumentu i pozostawia wariancję zwracanego typu bez zmian.
Uwaga
Pozwala to również wyjaśnić, jakie jest zachowanie wariancji tablic; Pobieranie elementów za pośrednictwem operatora dostępu do elementu odpowiada wywołaniu funkcji typu (Int -> TItem)
, gdzie TItem
jest typem elementów w tablicy. Ponieważ ta funkcja jest niejawnie przekazywana podczas przekazywania tablicy, wynika z tego, że tablice muszą być kowariantne w ich typie elementu. Te same zagadnienia dotyczą również krotki, które są niezmienne, a tym samym kowariantne w odniesieniu do każdego typu elementu.
Jeśli tablice nie były niezmienne, istnienie konstrukcji, która pozwoliłaby ustawić elementy w tablicy, a tym samym podjąć argument typu TItem
, oznaczałoby to, że tablice również muszą być kontrawariantne. Jedyną opcją dla typów danych, które obsługują pobieranie i ustawianie elementów, jest niezmienne, co oznacza, że nie ma żadnego podtypowania relacji; B[]
nie jest podtypem A[]
, nawet jeśli B
jest podtypem A
. Pomimo faktu, że tablice w Q# są niezmienne, są niezmienne, a nie kowariantne. Oznacza to na przykład, że nie można przekazać wartości typu (Qubit => Unit is Adj)[]
do elementu wywołującego, który wymaga argumentu typu (Qubit => Unit)[]
.
Utrzymywanie niezmienności tablic pozwala na większą elastyczność związaną ze sposobem obsługi i optymalizacji tablic w środowisku uruchomieniowym, ale może być możliwe poprawienie tego w przyszłości.