Subtipos y varianza
Q# admite solo algunos mecanismos de conversión. Las conversiones implícitas solo pueden producirse al aplicar operadores binarios, evaluar expresiones condicionales o construir un literal de matriz. En estos casos, se determina un supertipo común y las conversiones necesarias se realizan automáticamente. Además de estas conversiones implícitas, las conversiones explícitas a través de llamadas de función son posibles y a menudo necesarias.
Actualmente, la única relación de subtyping que existe se aplica a las operaciones. Intuitivamente, tiene sentido que se debe permitir que se sustituya una operación que admita más del conjunto necesario de functores. Concretamente, para dos tipos concretos TIn
y TOut
, la relación de subtyping es
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
donde A :> B
indica que B
es un subtipo de A
. Frase diferente, B
es más restrictivo que A
de modo que se pueda usar un valor de tipo B
siempre que se requiera un valor de tipo A
. Si un invocable se basa en un argumento (elemento) de ser de tipo A
, se puede sustituir un argumento de tipo B
de forma segura, ya que si proporciona todas las funcionalidades necesarias.
Este tipo de polimorfismo se extiende a las tuplas en que una tupla de tipo B
es un subtipo de un tipo de tupla A
si contiene el mismo número de elementos y el tipo de cada elemento es un subtipo del tipo de elemento correspondiente en A
. Esto se conoce como subtipado de profundidad . Actualmente no hay compatibilidad con subtipado de ancho, es decir, no hay ninguna relación de subtipo entre dos tipos struct
o un tipo de struct
y cualquier tipo integrado. La existencia del operador unwrap
, que permite extraer una tupla que contiene todos los elementos con nombre, evita esto.
Nota:
En lo que respecta a los invocables, si un invocable procesa un argumento de tipo A
, también es capaz de procesar un argumento de tipo B
. Si un invocable se pasa como argumento a otro invocable, debe ser capaz de procesar todo lo que pueda requerir la firma de tipo. Esto significa que si el invocable necesita poder procesar un argumento de tipo B
, cualquier invocable capaz de procesar un argumento más general de tipo A
se puede pasar de forma segura. Por el contrario, esperamos que si se requiere que el objeto al que se llama devuelva un valor de tipo A
, la promesa de devolver un valor de tipo B
es suficiente, ya que ese valor proporcionará todas las funcionalidades necesarias.
El tipo de función o operación se contravariante en su tipo de argumento y covariante en su tipo de valor devuelto.
A :> B
por lo tanto implica que para cualquier tipo concreto T1
,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
donde →
aquí puede significar una función o una operación, y se omiten las anotaciones de las características.
Sustituir A
con (B → T2)
y (T2 → A)
respectivamente, y sustituir B
por (A → T2)
y (T2 → B)
respectivamente, conduce a la conclusión de que, para cualquier tipo concreto 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))
Por inducción, sigue que cada direccionamiento indirecto adicional invierte la varianza del tipo de argumento y deja sin cambios la varianza del tipo devuelto.
Nota:
Esto también hace claro cuál debe ser el comportamiento de varianza de las matrices; recuperar elementos a través de un operador de acceso a elementos corresponde a invocar una función de tipo (Int -> TItem)
, donde TItem
es el tipo de los elementos de la matriz. Dado que esta función se pasa implícitamente al pasar una matriz, sigue que las matrices deben ser covariantes en su tipo de elemento. Las mismas consideraciones también tienen en cuenta las tuplas, que son inmutables y, por tanto, covariante con respecto a cada tipo de elemento.
Si las matrices no eran inmutables, la existencia de una construcción que le permitiría establecer elementos en una matriz y, por tanto, tomar un argumento de tipo TItem
, implicaría que las matrices también deben ser contravariantes. La única opción para los tipos de datos que admiten la obtención y configuración de elementos es, por lo tanto, ser invariable, lo que significa que no hay ninguna relación de subtipado; B[]
no se un subtipo de A[]
incluso si B
es un subtipo de A
. A pesar de que las matrices de Q# son inmutables, son invariables en lugar de covariante. Esto significa, por ejemplo, que un valor de tipo (Qubit => Unit is Adj)[]
no se puede pasar a un invocable que requiere un argumento de tipo (Qubit => Unit)[]
.
Mantener las matrices invariables permite una mayor flexibilidad relacionada con cómo se controlan y optimizan las matrices en tiempo de ejecución, pero puede ser posible revisarlas en el futuro.