Declarações de especialização
Conforme explicado na seção sobre declarações que podem ser chamadas, atualmente não há motivo para declarar explicitamente especializações de funções. Este tópico se aplica a operações e detalha como declarar as especializações necessárias para dar suporte a determinados funtores.
É um problema bastante comum na computação do Quantum exigir os adjacentes de determinada transformação. Muitos algoritmos do Quantum exigem uma operação e o adjacente dela para executar uma computação.
Q# emprega uma computação simbólica que pode gerar automaticamente a implementação adjacente correspondente de uma implementação de corpo específica. Essa geração é possível até mesmo para implementações que misturam livremente computação clássica do Quantum. No entanto, há algumas restrições que se aplicam nesse caso. Por exemplo, não há suporte para a geração automática por motivos de desempenho se a implementação usar variáveis mutáveis. Além disso, cada operação chamada dentro do corpo gera as necessidades adjacentes correspondentes para dar suporte ao próprio funtor Adjoint
.
Embora não seja possível desfazer com facilidade as medidas no caso de vários qubits, é possível combinar medidas para que a transformação aplicada seja unitária. Nesse caso, significa que, embora a implementação do corpo contenha medidas que por si só não dão suporte ao funtor Adjoint
, o corpo em sua totalidade é adjacente. No entanto, a geração automática da implementação adjacente falhará nesse caso. Por esse motivo, é possível especificar manualmente a implementação.
O compilador gera automaticamente implementações otimizadas para padrões comuns, como conjugações.
No entanto, uma especialização explícita pode ser desejável para definir uma implementação mais otimizada manualmente. É possível especificar qualquer implementação e qualquer número de implementações explicitamente.
Observação
O compilador não verifica se uma implementação especificada manualmente está correta.
No exemplo a seguir, a declaração de uma operação SWAP
, que troca o estado de dois qubits q1
e q2
, declara uma especialização explícita para a versão adjacente e a versão controlada da operação. Embora as implementações de Adjoint SWAP
e de Controlled SWAP
sejam, portanto, definidas pelo usuário, o compilador ainda precisa gerar a implementação para a combinação de ambos os funtores (Controlled Adjoint SWAP
, que é o mesmo que Adjoint Controlled SWAP
).
operation SWAP (q1 : Qubit, q2 : Qubit) : Unit
is Adj + Ctl {
body ... {
CNOT(q1, q2);
CNOT(q2, q1);
CNOT(q1, q2);
}
adjoint ... {
SWAP(q1, q2);
}
controlled (cs, ...) {
CNOT(q1, q2);
Controlled CNOT(cs, (q2, q1));
CNOT(q1, q2);
}
}
Diretivas de geração automática
Ao determinar como gerar uma especialização específica, o compilador prioriza as implementações definidas pelo usuário. Isso significa que, se uma especialização adjacente for definida pelo usuário e uma especialização controlada for gerada automaticamente, a especialização adjacente controlada será gerada com base no adjacente definido pelo usuário e vice-versa. Nesse caso, ambas as especializações são definidas pelo usuário. Como a geração automática de uma implementação adjacente está sujeita a mais limitação, a especialização adjacente controlada adota como comportamento padrão gerar a especialização controlada por meio da implementação explicitamente definida da especialização adjacente.
No caso da implementação SWAP
, a melhor opção é associar a especialização controlada para evitar condicionar desnecessariamente a execução do primeiro e do último CNOT
no estado dos qubits de controle.
Adicionar uma declaração explícita para a versão adjacente controlada que especifica uma diretiva de geração adequada força o compilador a gerar a especialização adjacente controlada com base na implementação especificada manualmente da versão controlada. Essa declaração explícita de uma especialização que deve ser gerada pelo compilador assume a forma
controlled adjoint invert;
e é inserida dentro da declaração de SWAP
.
Por outro lado, a inserção da linha
controlled adjoint distribute;
força o compilador a gerar a especialização com base na especialização adjacente definida (ou gerada). Confira esta proposta de inferência de especialização parcial para obter mais detalhes.
Para a operação SWAP
, há uma opção melhor.
SWAP
é auto-adjacente, ou seja, é inverso dele mesmo; a implementação definida do adjacente apenas chama o corpo de SWAP
. Você expressa isso com a diretiva
adjoint self;
Declarar a especialização adjacente dessa maneira garante que a especialização adjacente controlada que é inserida automaticamente pelo compilador apenas invoque a especialização controlada.
As seguintes diretivas de geração existem e são válidas:
Especialização | Diretiva(s) |
---|---|
body especialização: |
- |
adjoint especialização: |
self , invert |
controlled especialização: |
distribute |
controlled adjoint especialização: |
self , invert , distribute |
Não é coincidência que todas as diretivas de geração são válidas para uma especialização adjacente controlada; desde que os funtores comutem, o conjunto de diretivas de geração válidas para implementar a especialização de uma combinação de funtores é sempre a união do conjunto de geradores válidos para cada um deles.
Além das diretivas listadas anteriormente, a diretiva auto
é válida para todas as especializações, exceto body
; indica que o compilador deve escolher automaticamente uma diretiva de geração adequada.
Esta declaração
operation DoNothing() : Unit {
body ... { }
adjoint auto;
controlled auto;
controlled adjoint auto;
}
é equivalente a
operation DoNothing() : Unit
is Adj + Ctl { }
A anotação is Adj + Ctl
neste exemplo especifica as características da operação, que contêm as informações sobre quais funtores são compatíveis com uma operação específica.
Embora, para fins de legibilidade, seja recomendável anotar cada operação com uma descrição completa das características dela, o compilador insere ou conclui automaticamente a anotação com base em especializações explicitamente declaradas. Por outro lado, o compilador também gera especializações que não foram declaradas explicitamente, mas precisam existir com base nas características anotadas. Dizemos que a anotação determinada declarou implicitamente essas especializações. O compilador gera automaticamente as especializações necessárias se pode, escolhendo uma diretiva adequada. Então, Q# dá suporte à inferência das características da operação e das especializações existentes com base em anotações (parciais) e especializações explicitamente definidas.
De certa forma, as especializações são semelhantes a sobrecargas individuais para o mesmo elemento que pode ser chamado, com a ressalva de que determinadas restrições se aplicam às sobrecargas que você consegue declarar.