Subdigitação e variância
O Q# dá suporte apenas a alguns mecanismos de conversão. As conversões implícitas podem ocorrer somente ao aplicar operadores binárias, ao avaliar expressões condicionais e ao construir um literal de matriz. Nesses casos, um supertipo comum é determinado e as conversões necessárias são executadas automaticamente. Além dessas conversões implícitas, as conversões explícitas por chamadas de função são possíveis e, muitas vezes, necessárias.
No momento, a única relação de subtipo existente se aplica a operações. Intuitivamente, faz sentido poder substituir uma operação que dê suporte a um conjunto de functores maior do que o necessário. Concretamente, para os tipos concretos TIn
e TOut
, a relação de subdigitação é
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
onde A :> B
indica que B
é um subtipo de A
. Ou seja, B
é mais restritivo do que A
, de forma que um valor do tipo B
possa ser usado sempre que um valor do tipo A
for necessário. Se um callable se basear no argumento (item) de ser do tipo A
, um argumento do tipo B
poderá ser substituído com segurança, pois fornece todos os recursos necessários.
Esse tipo de polimorfismo se aplica às tuplas, pois um tipo de tupla B
é um subtipo do tipo de tupla A
quando contém o mesmo número de itens e o tipo de cada item é um subtipo do tipo de item correspondente em A
. Isso é conhecido como subdigitação de profundidade. No momento, não há suporte para o subtipo de largura. Ou seja, não há nenhuma relação de subtipo entre dois tipos definidos pelo usuário ou um tipo definido pelo usuário e qualquer tipo interno. Isso é impedido pela existência do operador unwrap
, que permite extrair uma tupla contendo todos os itens nomeados e anônimos.
Observação
Em relação aos chamadores, se um chamável processar um argumento do tipo A
, ele também será capaz de processar um argumento do tipo B
. Se um chamável for passado como um argumento para outro chamável, ele precisará ter a capacidade de processar qualquer coisa que a assinatura do tipo exigir. Isso significa que, se o chamável precisar da capacidade de processar um argumento do tipo B
, qualquer chamável com a capacidade de processar um argumento mais geral do tipo A
poderá ser passado com segurança. Por outro lado, esperamos que, se exigirmos que o chamável passado retorne um valor do tipo A
, a promessa de retornar um valor do tipo B
seja suficiente, pois esse valor fornecerá todas as funcionalidades necessárias.
A operação ou o tipo de função é contravariante em seu tipo de argumento e covariante em seu tipo de retorno. Portanto, A :> B
sugere que, para qualquer tipo T1
concreto,
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
onde →
aqui pode significar uma função ou operação e omitimos quaisquer anotações das características.
A substituição de A
por (B → T2)
e (T2 → A)
, respectivamente, e a substituição de B
por (A → T2)
e (T2 → B)
, respectivamente, leva à conclusão de que, para qualquer 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))
Por indução, como consequência, cada indireção adicional reverte a variância do tipo de argumento e deixa a variação do tipo de retorno inalterada.
Observação
Isso também esclarece qual deve ser o comportamento da variação das matrizes; a recuperação de itens por um operador de acesso a itens corresponde à invocação de uma função do tipo (Int -> TItem)
, em que TItem
é o tipo dos elementos na matriz. Como essa função é transmitida implicitamente ao passar uma matriz, como consequência, as matrizes precisam ser covariantes em seu tipo de item. As mesmas considerações também contêm tuplas, que são imutáveis e, portanto, a covariante referente a cada tipo de item.
Se as matrizes não fossem imutáveis, a existência de um constructo que permitisse definir itens em uma matriz e, portanto, usar um argumento do tipo TItem
, significaria que as matrizes também precisariam ser contravariantes. A única opção para tipos de dados que dão suporte à obtenção e configuração de itens é, portanto, invariável, o que significa que não há nenhuma relação de subdigitação; B[]
não é um subtipo de A[]
, mesmo que B
seja um subtipo deA
. Apesar das matrizes em Q# serem imutáveis, elas são invariantes em vez de covariantes. Isso, por exemplo, significa que um valor do tipo (Qubit => Unit is Adj)[]
não pode ser passado a um chamável que exija um argumento do tipo (Qubit => Unit)[]
.
Manter as matrizes invariáveis permite mais flexibilidade relacionada ao modo como as matrizes são tratadas e otimizadas no tempo de execução, mas pode ser possível revisá-las no futuro.