Compartir a través de


Subtipos y varianza

Q# solo admite algunos mecanismos de conversión. Las conversiones implícitas solo se pueden producir al aplicar operadores binarios, al evaluar expresiones condicionales y al 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, es posible la conversión explícita mediante llamadas de función y a menudo es necesaria.

En la actualidad, la única relación de subtipos que existe se aplica a las operaciones. Intuitivamente, tiene sentido que se deba permitir que se sustituya una operación que admita más que el conjunto necesario de functores. Específicamente, para dos tipos determinados cualquiera, TIn y TOut, la relación de subtipos 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. Con otras palabras, B es más restrictivo que A para que se pueda usar un valor de tipo B siempre que se requiera un valor de tipo A. Si un invocable se basa en que un argumento (elemento) sea de tipo A, se puede sustituir por un argumento de tipo B de forma segura, ya que proporciona todas las funcionalidades necesarias.

Este tipo de polimorfismo se extiende a las tuplas, en las que un tipo de tupla 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 subtipos profundos. Actualmente, no hay compatibilidad con los subtipos anchos, es decir, no hay ninguna relación de subtipo entre dos tipos definidos por el usuario o un tipo definido por el usuario y cualquier tipo integrado. La existencia del operador unwrap, que permite extraer una tupla que contiene todos los elementos con nombre y anónimos, impide esto.

Nota

En cuanto 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 cualquier cosa que pueda requerir la signatura del tipo. Esto significa que si el invocable debe ser capaz de procesar un argumento de tipo B, se puede pasar de forma segura cualquier invocable que sea capaz de procesar un argumento más general de tipo A. Por el contrario, esperamos que si se requiere que el invocable que se ha pasado 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 la función o la operación es contravariante en su tipo de argumento y covariante en su tipo de valor devuelto. Por lo tanto, A :> B implica que, para cualquier tipo T1 específico:

    (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 características. Sustituir A por (B → T2) y (T2 → A) respectivamente, y sustituir B por (A → T2) y (T2 → B) respectivamente, nos lleva a la conclusión de que, para cualquier tipo T2 específico:

    ((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, se cumple que cada direccionamiento indirecto adicional invierte la varianza del tipo de argumento y deja sin cambios la varianza del tipo de valor devuelto.

Nota

Esto también aclara cuál debe ser el comportamiento de varianza de las matrices; recuperar elementos mediante un operador de acceso a los elementos se 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, nos lleva a que las matrices deben ser covariantes en su tipo de elemento. También se mantienen las mismas consideraciones para las tuplas, que son inmutables y, por tanto, covariantes con respecto a cada tipo de elemento. Si las matrices no fueran inmutables, la existencia de una construcción que permitiese 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 obtener y establecer elementos es, por lo tanto, ser invariantes, lo que significa que no hay ninguna relación de subtipos; B[]no es un subtipo de A[] aunque B sea un subtipo de A. A pesar del hecho de que las matrices en Q# son inmutables, son invariantes en lugar de covariantes. Por ejemplo, esto significa que no se puede pasar un valor de tipo (Qubit => Unit is Adj)[] a un invocable que requiera un argumento de tipo (Qubit => Unit)[]. Mantener las matrices invariantes permite una mayor flexibilidad en cuanto a cómo se controlan y optimizan las matrices en el entorno de ejecución, pero es posible que se revise en el futuro.