Compartilhar via


Parametrizações de tipo

Q# dá suporte a operações e funções com parâmetros de tipo. As bibliotecas padrão Q# fazem uso intenso de callables parametrizados por tipo para fornecer uma série de abstrações úteis, incluindo funções como Mapped e Fold familiares de 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 das matrizes de entrada e saída. Como os tipos exatos não alteram a implementação da função Mapped, faz sentido que seja possível definir essa implementação para tipos de item arbitrários; queremos definir um modelo de de fábrica ou que, considerando os tipos concretos para os itens na matriz de entrada e saída, retorna a implementação de função correspondente. Essa noção é formalizada na forma de parâmetros de tipo.

Concretização

Qualquer operação ou declaração de função pode especificar um ou mais parâmetros de tipo que podem ser usados como tipos, ou parte dos tipos, da entrada ou saída do callable, ou ambos. As exceções são pontos de entrada, que devem 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 callable devem ser do mesmo tipo.

Um callable parametrizado por tipo precisa ser concretizado, ou seja, deve ser fornecido com os argumentos de 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 é considerado concreto se for um dos tipos internos, um tipo de struct ou se for concreto dentro do escopo atual. O exemplo a seguir ilustra o que significa que um tipo seja concreto dentro do escopo atual e é explicado com mais detalhes abaixo:

    function Mapped<'T1, 'T2> (
        mapper : 'T1 -> 'T2,
        array : 'T1[]
    ) : 'T2[] {

        mutable mapped = new 'T2[Length(array)];
        for (i in IndexRange(array)) {
            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. Ele usa 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 verdadeiro; isso geralmente é conhecido como a versão de controle clássico de op.

A função Mapped usa uma matriz de um tipo de item arbitrário 'T1 como argumento, aplica a função mapper fornecida a cada item e retorna uma nova matriz de tipo 'T2[] que contém 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; parâmetros de tipo para diferentes callables podem ter o mesmo nome, e o nome escolhido só é visível e relevante dentro da definição desse callable.

A função AllCControlled usa uma matriz de operações e retorna uma nova matriz que contém as versões de controle clássico dessas operações. A chamada de Mapped resolve seu parâmetro de tipo 'T1 para 'T3 => Unite 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 implicitamente definidos pelo argumento da expressão de chamada. Argumentos de tipo também podem ser especificados explicitamente, como é feito para CControlled na mesma linha. A CControlled<'T3> de concretização explícita é 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 de invocação de AllCControlled. Isso significa que assim que o ponto de entrada do programa - que não pode ser parametrizado por tipo - é conhecido, assim como o tipo concreto 'T3 para cada chamada para AllCControlled, de modo que uma implementação adequada para essa resolução de tipo específica possa ser gerada. Depois que o ponto de entrada para um programa for conhecido, todos os usos de parâmetros de tipo poderão ser eliminados em tempo de compilação. Nos referimos a esse processo como 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 resulta em um loop infinito, ele serve para fins de ilustração. Foo invoca-se com a versão de controle 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), em que 'TArg é o parâmetro de tipo da chamada atual. De forma concreta, suponha que Foo seja invocado com o H de operação e um argumento arg do tipo Qubit. Foo se invoca com um argumento de tipo (Bool, Qubit), que invoca 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 callables parametrizados por tipo. Cada callable precisa ser invocado com o mesmo conjunto de argumentos de tipo depois de percorrer o ciclo.

Observação

Seria possível ser menos restritivo e exigir que, para cada callable no ciclo, haja um número finito de ciclos após os quais ele é invocado com o conjunto original de argumentos de tipo, como o caso da 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 parâmetro callable concreto sem qualquer tipo de parâmetro, tal callable garante que os callables parametrizados de tipo dentro desse ciclo sejam sempre chamados com um conjunto fixo de argumentos de tipo.