Sottotipizzazione e varianza
Q# supporta solo alcuni meccanismi di conversione. Le conversioni implicite possono verificarsi solo quando si applicano operatori binari, si valutano espressioni condizionali o si costruisce un valore letterale di matrice. In questi casi, viene determinato un supertipo comune e le conversioni necessarie vengono eseguite automaticamente. A parte queste conversioni implicite, le conversioni esplicite tramite chiamate di funzione sono possibili e spesso necessarie.
Attualmente, l'unica relazione di sottotipizzazione esistente si applica alle operazioni. Intuitivamente, è opportuno consentire di sostituire un'operazione che supporta più del set di functor richiesto. Nella pratica, per due qualsiasi tipi concreti TIn
e TOut
, la relazione di sottotipizzazione è
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
dove A :> B
indica che B
è un sottotipo di A
. In altre parole, B
è più restrittivo di A
tanto che un valore di tipo B
può essere usato ovunque sia necessario un valore di tipo A
. Se un elemento chiamabile si basa su un argomento (elemento) che deve essere di tipo A
, un argomento di tipo B
può essere sostituito senza problemi perché fornisce tutte le funzionalità necessarie.
Questo tipo di polimorfismo si estende alle tuple in quanto una tupla di tipo B
è un sottotipo di un tipo A
di tupla se contiene lo stesso numero di elementi e il tipo di ogni elemento è un sottotipo del tipo di elemento corrispondente in A
. Questa condizione è nota come sottotipizzazione della profondità. Attualmente non è disponibile alcun supporto per la sottotipizzazione di larghezza, ovvero non esiste alcuna relazione di sottotipo tra due tipi definiti dall'utente o un tipo definito dall'utente e qualsiasi tipo predefinito. L'esistenza dell'operatore unwrap
, che consente di estrarre una tupla contenente tutti gli elementi denominati e anonimi, impedisce questa operazione.
Nota
Per quanto riguarda i chiamabili, se un oggetto chiamabile elabora un argomento di tipo A
, è anche in grado di elaborare un argomento di tipo B
. Se un oggetto chiamabile viene passato come argomento a un altro chiamabile, deve essere in grado di elaborare qualsiasi elemento necessario per la firma del tipo. Ciò significa che se il chiamabile deve essere in grado di elaborare un argomento di tipo B
, qualsiasi chiamabile in grado di elaborare un argomento più generale di tipo A
può essere passato in modo sicuro. Al contrario, si prevede che, se è necessario che il callable passato restituisca un valore di tipo A
, la promessa di restituire un valore di tipo B
è sufficiente, poiché tale valore fornirà tutte le funzionalità necessarie.
Il tipo di operazione o funzione è controvariante nel tipo di argomento e covariante nel tipo restituito.
A :> B
implica quindi che per qualsiasi tipo concreto T1
,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
dove →
qui può significare una funzione o un'operazione e vengono omesse eventuali annotazioni per le caratteristiche.
A
Sostituendo rispettivamente con (B → T2)
e (T2 → A)
, e sostituendo B
rispettivamente con (A → T2)
e (T2 → B)
, porta alla conclusione che, per qualsiasi tipo T2
concreto ,
((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))
Per induzione, ne consegue che ogni riferimento indiretto aggiuntivo inverte la varianza del tipo di argomento senza modificare la varianza del tipo restituito.
Nota
Ciò chiarisce anche quale deve essere il comportamento di varianza delle matrici. Il recupero di elementi tramite un operatore di accesso agli elementi corrisponde alla chiamata a una funzione di tipo (Int -> TItem)
, dove TItem
è il tipo degli elementi nella matrice. Poiché questa funzione viene passata in modo implicito quando si passa una matrice, ne consegue che le matrici devono essere covarianti nel tipo di elemento. Le stesse considerazioni valgono anche le tuple, che sono non modificabili e quindi sono covarianti rispetto a ogni tipo di elemento.
Se le matrici non erano modificabili, l'esistenza di un costrutto che consente di impostare gli elementi in una matrice e quindi accettare un argomento di tipo TItem
, implica che anche le matrici devono essere controvarianti. La sola opzione per i tipi di dati che supportano l'acquisizione e l'impostazione degli elementi è quindi che siano invarianti, vale a dire che non esiste alcuna relazione di sottotipizzazione. B[]
non è un sottotipo di A[]
anche se B
è un sottotipo di A
. Nonostante il fatto che le matrici in Q# siano immutabili, sono invarianti anziché covarianti. Ciò significa, ad esempio, che un valore di tipo (Qubit => Unit is Adj)[]
non può essere passato a un oggetto chiamabile che richiede un argomento di tipo (Qubit => Unit)[]
.
Mantenere le matrici invarianti consente una maggiore flessibilità nella gestione e ottimizzazione delle matrici nel runtime, ma è questo in futuro potrebbe cambiare.