Sous-typage et variance
Q# prend en charge seulement quelques mécanismes de conversion. Les conversions implicites peuvent uniquement intervenir lors de l’application d’opérateurs binaires, de l’évaluation d’expressions conditionnelles et de la construction d’un littéral de tableau. Dans ces différents cas, un supertype commun est déterminé et les conversions nécessaires sont effectuées automatiquement. Outre ces conversions implicites, des conversions explicites via des appels de fonction sont possibles et souvent nécessaires.
Actuellement, la seule relation de sous-typage existante s’applique aux opérations. Intuitivement, il apparaît logique de pouvoir substituer une opération qui prend en charge plus que l’ensemble de foncteurs requis. Concrètement, pour deux types TIn
concrets et TOut
, la relation de sous-typage 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
et dès lors, une valeur de type B
peut être utilisée partout où une valeur de type A
est requise. Si un callable s’appuie sur un argument (élément) de type A
, un argument de type B
peut être substitué en toute sécurité dans la mesure où il fournit les fonctionnalités nécessaires.
Ce type de polymorphisme s’étend aux tuples en ce sens qu’un tuple de type B
est un sous-type d’un type de 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 que l’on appelle le sous-typage en profondeur. Actuellement, le sous-typage en largeur n’est pas pris en charge. Cela signifie qu’il n’existe aucune relation de sous-type entre deux types définis par l’utilisateur ou un type défini par l’utilisateur et un type intégré. L’existence de l’opérateur unwrap
, qui vous permet d’extraire un tuple contenant tous les éléments nommés et anonymes, empêche cela.
Notes
Concernant les callables, si un callable traite un argument de type A
, il est également en mesure de traiter un argument de type B
. Si un callable est transmis en tant qu’argument à un autre callable, il doit être capable de traiter tout ce que la signature de type peut exiger. Cela signifie que si le callable doit être en mesure de traiter un argument de type B
, tout callable capable de traiter un argument plus général de type A
peut être transmis en toute sécurité. À l’inverse, nous pensons que si nous exigeons du callable transmis qu’il retourne une valeur de type A
, la promesse de retourner une valeur de type B
est suffisante, puisque cette valeur fournira toutes les fonctionnalités nécessaires.
Le type d’opération ou de fonction est contravariant en termes de type d’argument et covariant en termes de type de retour.
A :> B
implique donc cela pour tout type T1
concret,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
où ici, →
peut correspondre à une fonction ou une opération, et nous omettons toutes les annotations relatives aux caractéristiques.
Substituer A
avec (B → T2)
et (T2 → A)
respectivement, et substituer B
avec (A → T2)
et (T2 → B)
respectivement, mène à 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 s'ensuit que chaque indirection supplémentaire inverse la variance du type d'argument et laisse la variance du type de retour inchangée.
Notes
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
correspond au type des éléments du tableau. Cette fonction étant transmise implicitement lors de la transmission d’un tableau, il s’ensuit que les tableaux doivent être covariants en termes de type d’élément. Les mêmes considérations s'appliquent également aux 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 vous permettant de définir des éléments dans un tableau, et donc d’accepter un argument de type TItem
, impliquerait que les tableaux doivent aussi être contravariants. La seule option pour les types de données qui prennent en charge l’obtention et la définition d’éléments consiste donc à être invariants, ce qui signifie qu’il n’existe aucune relation de sous-typage. B[]
n’est pas un sous-type de A[]
, même si B
est un sous-type de A
. Bien que les tableaux dans Q# soient 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 transmise à un callable nécessitant un argument de type (Qubit => Unit)[]
.
La conservation des tableaux invariants offre plus de flexibilité en ce qui concerne la façon dont les tableaux sont gérés et optimisés dans le runtime, mais il sera peut être possible de revoir cela à l’avenir.