Sous-typage et variance
Q# ne prend en charge que quelques mécanismes de conversion. Les conversions implicites peuvent se produire uniquement lors de l’application d’opérateurs binaires, de l’évaluation des expressions conditionnelles ou de la construction d’un littéral de tableau. Dans ces cas, un supertype commun est déterminé et les conversions nécessaires sont effectuées automatiquement. Outre ces conversions implicites, les conversions explicites via les appels de fonction sont possibles et souvent nécessaires.
Actuellement, la seule relation de sous-saisie qui existe s’applique aux opérations. Intuitivement, il est logique que l’on puisse remplacer une opération qui prend en charge plus que l’ensemble requis de functors. Concrètement, pour deux types concrets TIn
et TOut
, la relation de sous-frappe est
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
où A :> B
indique que B
est un sous-type de A
. Autrement dit, B
est plus restrictif que A
afin qu’une valeur de type B
puisse être utilisée partout où une valeur de type A
est requise. Si un appelant s’appuie sur un argument (élément) de type A
, un argument de type B
peut être substitué en toute sécurité, car s’il fournit toutes les fonctionnalités nécessaires.
Ce type de polymorphisme s’étend aux tuples dans lequel un tuple de type B
est un sous-type d’un type tuple A
s’il contient le même nombre d’éléments et que le type de chaque élément est un sous-type du type d’élément correspondant dans A
. C’est ce qu’on appelle sous-frappe de profondeur. Il n’existe actuellement aucune prise en charge des sous-types de largeur , autrement dit, il n’existe aucune relation de sous-type entre deux types struct
ou un type struct
et n’importe quel type intégré. L’existence de l’opérateur unwrap
, qui vous permet d’extraire un tuple contenant tous les éléments nommés, empêche cela.
Remarque
En ce qui concerne les appelants, si un appelant traite un argument de type A
, il est également capable de traiter un argument de type B
. Si un appelant est passé en tant qu’argument à un autre appelant, il doit être capable de traiter tout ce dont la signature de type peut nécessiter. Cela signifie que si l’appelant doit être en mesure de traiter un argument de type B
, tout appelant capable de traiter un argument plus général de type A
peut être passé en toute sécurité. À l’inverse, nous nous attendons à ce que si nous exigeons que l’appelant transmis retourne une valeur de type A
, la promesse de retourner une valeur de type B
est suffisante, car cette valeur fournira toutes les fonctionnalités nécessaires.
L’opération ou le type de fonction est contravariant dans son type d’argument et covariant dans son type de retour.
A :> B
implique donc que pour tout type concret T1
,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
où →
ici peut signifier une fonction ou une opération, et nous omettez toutes les annotations pour les caractéristiques.
Le remplacement de A
par (B → T2)
et de (T2 → A)
respectivement, et le remplacement de B
par (A → T2)
et de (T2 → B)
respectivement conduit à la conclusion que, pour tout type concret 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))
Par induction, il suit que chaque indirection supplémentaire inverse la variance du type d’argument et laisse la variance du type de retour inchangée.
Remarque
Cela permet également de clarifier le comportement de variance des tableaux ; la récupération d’éléments via un opérateur d’accès aux éléments correspond à l’appel d’une fonction de type (Int -> TItem)
, où TItem
est le type des éléments dans le tableau. Étant donné que cette fonction est implicitement passée lors du passage d’un tableau, elle suit que les tableaux doivent être covariants dans leur type d’élément. Les mêmes considérations tiennent également pour les tuples, qui sont immuables et donc covariants par rapport à chaque type d’élément.
Si les tableaux n’étaient pas immuables, l’existence d’une construction qui vous permettrait de définir des éléments dans un tableau, et donc de prendre un argument de type TItem
, implique que les tableaux doivent également être contravariants. La seule option pour les types de données qui prennent en charge l’obtention et la définition d’éléments est donc d’être invariant, ce qui signifie qu’il n’y a aucune relation de sous-saisie ; B[]
n’est pas un sous-type de A[]
même si B
est un sous-type de A
. Malgré le fait que les tableaux dans Q# sont immuables , ils sont invariants plutôt que covariants. Cela signifie, par exemple, qu’une valeur de type (Qubit => Unit is Adj)[]
ne peut pas être passée à un appelant qui nécessite un argument de type (Qubit => Unit)[]
.
La conservation des tableaux invariant permet une plus grande flexibilité liée à la façon dont les tableaux sont gérés et optimisés dans le runtime, mais il peut être possible de le réviser à l’avenir.