Partilhar via


Parametrizações de tipo

Q# suporta operações e funções parametrizadas por tipo. As Q# bibliotecas padrão utilizam intensivamente os callables parametrizados de tipo 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 numa matriz e devolve uma nova matriz com os valores calculados. Esta funcionalidade pode ser perfeitamente descrita sem especificar os tipos de itens das matrizes de entrada e saída. Uma vez que os tipos exatos não alteram a implementação da função Mapped, faz sentido que seja possível definir esta implementação para tipos de itens arbitrários; queremos definir uma fábrica ou modelo que, tendo em conta os tipos concretos dos itens na matriz de entrada e saída, devolve 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 utilizados como os tipos, ou parte dos tipos, da entrada ou saída do callable, ou ambos. As exceções são pontos de entrada, que têm de ser concretos e não podem ser de tipo parametrizado. Os nomes dos parâmetros 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 têm de ser do mesmo tipo.

Um tipo parametrizado callable tem de ser concretizado, ou seja, tem de ser fornecido com os argumentos de tipo necessários antes de poder ser atribuído ou transmitido como argumento, de modo a que todos os parâmetros de tipo possam ser substituídos por tipos de betão. Um tipo é considerado concreto se for um dos tipos incorporados, um tipo definido pelo utilizador ou se for concreto no âmbito atual. O exemplo seguinte ilustra o que significa para um tipo ser concreto no âmbito 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 Microsoft.Quantum.Canon espaço de nomes. Utiliza uma operação op do tipo 'TIn => Unit como argumento e devolve uma nova operação do tipo (Bool, 'TIn) => Unit que aplica a operação original, desde que um bit clássico (do tipo Bool) esteja definido como verdadeiro; isto é frequentemente referido como a versão controlada clássicamente do op.

A função Mapped utiliza uma matriz de um tipo 'T1 de item arbitrário como argumento, aplica a função especificada mapper a cada item e devolve uma nova matriz de tipo 'T2[] que contém os itens mapeados. É definido no Microsoft.Quantum.Array espaço de nomes. Para efeitos do exemplo, os parâmetros de tipo são numerados para evitar tornar o debate mais confuso ao dar o mesmo nome aos parâmetros de tipo em ambas as funções. Isto não é necessário; os 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 utiliza uma matriz de operações e devolve uma nova matriz que contém as versões controladas clássicamente destas operações. A chamada de resolve o parâmetro de Mapped tipo para 'T3 => Unite o parâmetro 'T2 de tipo para (Bool,'T3) => Unit'T1 . Os argumentos do tipo de resolução são inferidos pelo compilador com base no tipo do argumento especificado. Dizemos que são implicitamente definidos pelo argumento da expressão de chamada. Os argumentos de tipo também podem ser especificados explicitamente, como é feito CControlled na mesma linha. A concretização CControlled<'T3> explícita é necessária quando os argumentos de tipo não podem ser inferidos.

O tipo 'T3 é concreto no contexto de AllCControlled, uma vez que é conhecido por cada invocação de AllCControlled. Isto significa que assim que o ponto de entrada do programa - que não pode ser de tipo parametrizado - também é do tipo 'T3 concreto para cada chamada para AllCControlled, de modo a que possa ser gerada uma implementação adequada para essa resolução de tipo específica. Assim que o ponto de entrada de um programa for conhecido, todas as utilizações de parâmetros de tipo podem ser eliminadas no tempo de compilação. Referimo-nos a este processo como monomorfização.

São necessárias algumas restrições para garantir que isto pode ser feito no tempo de compilação em vez de apenas no tempo de execução.

Restrições

Considere o exemplo seguinte:

    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á num ciclo infinito, serve para efeitos de ilustração. Foo invoca-se com a versão clássica controlada da operação op original que foi transmitida, bem como uma cadeia de identificação que contém um bit clássico aleatório, além do argumento original.

Para cada iteração na recursão, o parâmetro 'TArg de tipo da próxima chamada é resolvido para (Bool, 'TArg), em que 'TArg é o parâmetro de tipo da chamada atual. Concretamente, suponha que Foo é invocado com a operação H e um argumento arg do tipo Qubit. Foo em seguida, invocar-se-á com um argumento (Bool, Qubit)de tipo , que, em seguida, invocará Foo com um argumento (Bool, (Bool, Qubit))de tipo e assim sucessivamente. É evidente que, neste caso Foo , não pode ser monomorfizado em tempo de compilação.

Aplicam-se restrições adicionais a ciclos no gráfico de chamadas que envolvem apenas chamadas com tipo parametrizado. Cada callable tem de ser invocado com o mesmo conjunto de argumentos de tipo depois de percorrer o ciclo.

Nota

Seria possível ser menos restritivo e exigir que, para cada callable no ciclo, exista um número finito de ciclos após o qual é 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 é aplicado. Tenha em atenção que, para ciclos que envolvam pelo menos um parâmetro callable concreto sem qualquer tipo, tal callable irá garantir que os callables com tipo parametrizado dentro desse ciclo são sempre chamados com um conjunto fixo de argumentos de tipo.