Partilhar via


Parametrizações de tipo

Q# suporta operações e funções parametrizadas por tipo. As bibliotecas padrão Q# fazem uso intensivo de chamadas parametrizadas para fornecer uma série de abstrações úteis, incluindo funções como Mapped e Fold que são 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 calculados. Essa funcionalidade pode ser perfeitamente descrita sem especificar os tipos de itens 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 itens arbitrários; Queremos definir um de fábrica ou modelo que, dados os tipos concretos para os itens na matriz de entrada e saída, retorna a implementação da função correspondente. Esta noção é formalizada sob a 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, de entrada ou saída do chamável, ou ambos. As exceções são os pontos de entrada, que devem ser concretos e não podem ser parametrizados. Os nomes dos parâmetros de tipo começam com um tick (') e podem aparecer várias vezes nos tipos de entrada e saída. Todos os argumentos que correspondem ao mesmo parâmetro type na assinatura chamável devem ser do mesmo tipo.

Um tipo de chamada parametrizado precisa ser concretizado, isto é, deve ser fornecido com os argumentos de tipo necessários antes de poder 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 struct ou se for concreto dentro do escopo atual. O exemplo a seguir ilustra o que significa para um tipo ser 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; Isto é muitas vezes referido como a versão classicamente controlada do op.

A função Mapped usa uma matriz de um tipo de item arbitrário 'T1 como argumento, aplica a função de mapper dada a cada item e retorna uma nova matriz de tipo 'T2[] contendo os itens mapeados. Ele é definido no namespace Microsoft.Quantum.Array. Para o propósito 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 chamáveis podem ter o mesmo nome, e o nome escolhido só é visível e relevante dentro da definição desse chamável.

A função AllCControlled usa uma matriz de operações e retorna uma nova matriz contendo as versões classicamente controladas dessas operações. A chamada de Mapped resolve seu parâmetro type 'T1 para 'T3 => Unite seu parâmetro type 'T2 para (Bool,'T3) => Unit. Os argumentos de tipo de resolução são inferidos pelo compilador com base no tipo do argumento dado. Dizemos que são implicitamente definidas 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 no contexto da AllCControlled, uma vez que é conhecido por cada invocação de AllCControlled. Isso significa que, assim que o ponto de entrada do programa - que não pode ser parametrizado - é conhecido, o mesmo acontece com o tipo concreto 'T3 para cada chamada para AllCControlled, de modo que uma implementação adequada para essa resolução de tipo específico pode ser gerada. Uma vez que o ponto de entrada para um programa é conhecido, todos os usos de parâmetros de tipo podem ser eliminados em tempo de compilação. Referimo-nos a este processo como 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, ela serve para fins de ilustração. Foo invoca-se com a versão classicamente controlada do op de operação original que foi passado, bem como uma tupla contendo um bit clássico aleatório, além do argumento original.

Para cada iteração na recursão, o parâmetro type 'TArg da próxima chamada é resolvido para (Bool, 'TArg), onde 'TArg é o parâmetro type da chamada atual. Concretamente, suponha que Foo é invocado com a operação H e um argumento arg do tipo Qubit. Foo então invoca a si mesmo com um argumento de tipo (Bool, Qubit), que então invoca Foo com um argumento de tipo (Bool, (Bool, Qubit)), e assim por diante. Claramente, neste caso, Foo não pode ser monomorfizado em tempo de compilação.

Restrições adicionais se aplicam a ciclos no gráfico de chamadas que envolvem apenas chamadas parametrizadas por tipo. Cada chamável 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 chamável no ciclo, haja um número finito de ciclos após o qual 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, é aplicado o requisito mais restritivo. Observe que, para ciclos que envolvem pelo menos um chamável concreto sem qualquer parâmetro de tipo, tal chamável garante que os chamáveis parametrizados dentro desse ciclo sejam sempre chamados com um conjunto fixo de argumentos de tipo.