Parametrizações de tipo
Q# aceita operações e funções com parâmetros de tipo. As bibliotecas padrão Q# fazem uso intenso de tipos parametrizados de chamáveis para fornecer um host de abstrações úteis, incluindo funções como Mapped
e Fold
, que são familiares em linguagens funcionais.
Para motivar o conceito de parametrizações de tipo, considere o exemplo da função Mapped
, que aplica uma determinada função a cada valor em uma matriz e retorna uma nova matriz com os valores computados. Essa funcionalidade pode ser perfeitamente descrita sem especificar os tipos de item da matriz de entrada e saída. Como os tipos exatos não alteram a implementação da função Mapped
, a possibilidade de definir essa implementação para tipos de itens arbitrários faz sentido. Queremos definir um alocador ou um modelo que, considerando os tipos concretos dos itens na matriz de entrada e saída, retorne a implementação da função correspondente. Essa noção é formalizada na forma de parâmetros de tipo.
Concretização
Qualquer declaração de operação ou função pode especificar um ou mais parâmetros de tipo que podem ser usados como os tipos ou parte dos tipos de entrada e/ou de saída do chamável. A exceção são os pontos de entrada, que precisam ser concretos e não podem ser parametrizados por tipo. Os nomes de parâmetro de tipo começam com um tique (') e podem aparecer várias vezes nos tipos de entrada e saída. Todos os argumentos que correspondem ao mesmo parâmetro de tipo na assinatura de chamada devem ser do mesmo tipo.
Um chamável parametrizado por tipo precisa ser concretizado, ou seja, fornecido com os argumentos do tipo necessários para que possa ser atribuído ou passado como argumento, de modo que todos os parâmetros de tipo possam ser substituídos por tipos concretos. Um tipo será considerado concreto se for um dos tipos integrados, um tipo definido pelo usuário ou concreto no escopo atual. O exemplo a seguir ilustra o que significa que um tipo seja concreto dentro do escopo atual e é explicado mais detalhadamente abaixo:
function Mapped<'T1, 'T2> (
mapper : 'T1 -> 'T2,
array : 'T1[]
) : 'T2[] {
mutable mapped = new 'T2[Length(array)];
for (i in IndexRange(array)) {
set mapped w/= i <- mapper(array[i]);
}
return mapped;
}
function AllCControlled<'T3> (
ops : ('T3 => Unit)[]
) : ((Bool,'T3) => Unit)[] {
return Mapped(CControlled<'T3>, ops);
}
A função CControlled
é definida no namespace Microsoft.Quantum.Canon
. Ela recebe uma operação op
do tipo 'TIn => Unit
como argumento e retorna uma nova operação do tipo (Bool, 'TIn) => Unit
que aplica a operação original, desde que um bit clássico (do tipo Bool
) seja definido como true. Isso costuma ser chamado de versão de op
controlada de modo clássico.
A função Mapped
usa uma matriz de um tipo de item 'T1
arbitrário como argumento, aplica a função mapper
determinada a cada item e retorna uma nova matriz do tipo 'T2[]
contendo os itens mapeados. Ele é definido no namespace Microsoft.Quantum.Array
. Para fins do exemplo, os parâmetros de tipo são numerados para evitar tornar a discussão mais confusa, dando aos parâmetros de tipo em ambas as funções o mesmo nome. Isso não é necessário; os parâmetros de tipo para diferentes chamadores podem ter o mesmo nome, e o nome escolhido só é visível e relevante dentro da definição desse chamador.
A função AllCControlled
aceita uma matriz de operações e retorna uma nova matriz que contém as versões controladas de forma clássica dessas operações. A chamada de Mapped
resolve seu parâmetro de tipo 'T1
para 'T3 => Unit
e seu parâmetro de tipo 'T2
para (Bool,'T3) => Unit
. Os argumentos de tipo de resolução são inferidos pelo compilador com base no tipo do argumento fornecido. Dizemos que eles são definidos implicitamente pelo argumento da expressão de chamada. Os argumentos de tipo também podem ser especificados explicitamente como é feito para CControlled
na mesma linha. A concretização explícita CControlled<'T3>
é necessária quando os argumentos de tipo não podem ser inferidos.
O tipo 'T3
é concreto dentro do contexto de AllCControlled
, pois é conhecido por cada invocação de AllCControlled
. Isso significa que assim que o ponto de entrada do programa, que não pode ser parametrizado por tipo, é conhecido, também é conhecido o tipo concreto 'T3
de cada chamada a AllCControlled
, de modo que uma implementação adequada dessa resolução de tipo específica possa ser gerada. Depois que o ponto de entrada de um programa é conhecido, todos os tipos de uso de parâmetros de tipo podem ser eliminados em tempo de compilação. Chamamos esse processo de monomorfização.
Algumas restrições são necessárias para garantir que isso possa realmente ser feito em tempo de compilação em vez de apenas em tempo de execução.
Restrições
Considere o seguinte exemplo:
operation Foo<'TArg> (
op : 'TArg => Unit,
arg : 'TArg
) : Unit {
let cbit = RandomInt(2) == 0;
Foo(CControlled(op), (cbit, arg));
}
Ignorando que uma invocação de Foo
resultará em um loop infinito, ele atende ao propósito de ilustração.
Foo
invoca a si mesmo com a versão controlada de modo clássico da operação original op
que foi passada, bem como uma tupla que contém um bit clássico aleatório, além do argumento original.
Para cada iteração na recursão, o parâmetro de tipo 'TArg
da próxima chamada é resolvido para (Bool, 'TArg)
, onde 'TArg
é o parâmetro de tipo da chamada atual. Concretamente, suponha que Foo
seja invocado com a operação H
e um argumento arg
do tipo Qubit
.
Foo
em seguida, invocará a si mesmo com um argumento de tipo (Bool, Qubit)
, que, em seguida, invocará Foo
com um argumento de tipo (Bool, (Bool, Qubit))
e assim por diante. Claramente, nesse caso, Foo
não pode ser monomorfizado em tempo de compilação.
Restrições adicionais se aplicam a ciclos no grafo de chamadas que envolvem apenas chamáveis parametrizados por tipo. Cada chamador precisa ser invocado com o mesmo conjunto de argumentos de tipo depois de percorrer o ciclo.
Observação
É possível ser menos restritivo e exigir que, para cada chamável no ciclo, haja um número finito de ciclos, após os quais ele será invocado com o conjunto original de argumentos de tipo, como é o caso na seguinte função:
function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
Bar<'T2,'T3,'T1>(a2, a3, a1);
}
Para simplificar, o requisito mais restritivo é imposto. Observe que, para ciclos que envolvem pelo menos um chamável concreto sem nenhum parâmetro de tipo, esse tipo de chamável garantirá que os chamáveis parametrizados por tipo nesse ciclo sempre sejam chamados com um conjunto fixo de argumentos de tipo.