Dichiarazioni di specializzazione
Come illustrato nella sezione sulle dichiarazioni chiamabili, attualmente non esiste alcun motivo per dichiarare in modo esplicito le specializzazioni per le funzioni. Questo argomento si applica alle operazioni e illustra come dichiarare le specializzazioni necessarie per supportare determinati funtori.
Un problema comune nel calcolo quantistico consiste nel richiedere l'aggiunto di una determinata trasformazione. Molti algoritmi quantistici richiedono un'operazione e il relativo aggiunto per eseguire un calcolo.
Q# usa il calcolo simbolico che può generare automaticamente l'implementazione dell'aggiunto corrispondente per una particolare implementazione del corpo. Questa generazione è possibile anche per le implementazioni che combinano liberamente calcoli classici e quantistici. Esistono tuttavia alcune restrizioni che si applicano in questo caso. Se ad esempio l'implementazione usa variabili modificabili, la generazione automatica non è supportata per evitare problemi di prestazioni. Inoltre, per ogni operazione chiamata all'interno del corpo viene generato l'aggiunto corrispondente necessario per supportare il funtore Adjoint
stesso.
Anche se non è possibile annullare facilmente le misurazioni nel caso di più qubit, è possibile combinare le misure in modo che la trasformazione applicata sia unitaria. In questo caso, significa che, anche se l'implementazione del corpo contiene misurazioni che di per sé non supportano il funtore Adjoint
, il corpo nella sua interezza supporta l'aggiunzione. In questo caso però la generazione automatica dell'implementazione dell'aggiunto non riuscirà. Per questo motivo è possibile specificare manualmente l'implementazione.
Il compilatore genera automaticamente implementazioni ottimizzate per modelli comuni, ad esempio le coniugazioni.
Per definire manualmente un'implementazione più ottimizzata, può però essere utile una specializzazione esplicita. È possibile specificare un'implementazione qualsiasi e un numero qualsiasi di implementazioni in modo esplicito.
Nota
La correttezza di tale implementazione specificata manualmente non viene verificata dal compilatore.
Nell'esempio seguente nella dichiarazione per un'operazione SWAP
, che scambia lo stato di due qubit, q1
e q2
, viene dichiarata una specializzazione esplicita per la versione adjoint e la relativa versione controlled. Anche se le implementazioni per Adjoint SWAP
e Controlled SWAP
sono quindi definite dall'utente, il compilatore deve comunque generare l'implementazione per la combinazione di entrambi i funtori (Controlled Adjoint SWAP
, che è uguale a 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);
}
}
Direttive di generazione automatica
Quando si determina come generare una particolare specializzazione, il compilatore assegna la priorità alle implementazioni definite dall'utente. Questo significa che se una specializzazione adjoint è definita dall'utente e una specializzazione controlled viene generata automaticamente, la specializzazione controlled adjoint viene generata in base all'aggiunto definito dall'utente e viceversa. In questo caso, entrambe le specializzazioni sono definite dall'utente. Poiché la generazione automatica di un'implementazione adjoint è soggetta a maggiori limitazioni, la specializzazione controlled adjoint genera per impostazione predefinita la specializzazione controlled dell'implementazione definita in modo esplicito della specializzazione adjoint.
Nel caso dell'implementazione SWAP
, l'opzione migliore consiste nell'aggiungere la specializzazione controlled per evitare di condizionare inutilmente l'esecuzione del primo e dell'ultimo CNOT
nello stato dei qubit di controllo.
L'aggiunta di una dichiarazione esplicita per la versione controlled adjoint che specifica una direttiva di generazione appropriata impone al compilatore di generare la specializzazione controlled adjoint in base all'implementazione specificata manualmente della versione controlled. Il formato di tale dichiarazione esplicita di una specializzazione che deve essere generata dal compilatore è
controlled adjoint invert;
e la dichiarazione viene inserita all'interno della dichiarazione di SWAP
.
D'altra parte, l'inserimento della riga
controlled adjoint distribute;
forza il compilatore a generare la specializzazione in base alla specializzazione adjoint definita (o generata). Per maggiori dettagli, vedere questa proposta di inferenza di specializzazione parziale.
Per l'operazione SWAP
, è disponibile un'opzione migliore.
SWAP
è l'aggiunto automatico, ovvero il suo inverso. L'implementazione definita dell'aggiunto chiama semplicemente il corpo di SWAP
. Per esprimerlo, si usa la direttiva
adjoint self;
Dichiarando la specializzazione adjoint in questo modo si garantisce che la specializzazione controlled adjoint che viene inserita automaticamente dal compilatore richiami semplicemente la specializzazione controlled.
Sono disponibili le direttive di generazione seguenti valide:
Specializzazione | Direttiva/e |
---|---|
Specializzazione body : |
- |
Specializzazione adjoint : |
self , invert |
Specializzazione controlled : |
distribute |
Specializzazione controlled adjoint : |
self , invert , distribute |
Non è solo una coincidenza che tutte le direttive di generazione siano valide per una specializzazione controlled adjoint. Purché i funtori supportino lo scambio, il set di direttive di generazione valide per l'implementazione della specializzazione per una combinazione di funtori è sempre data dall'unione del set di generatori validi per ognuno di essi.
Oltre alle direttive elencate in precedenza, la direttiva auto
è valida per tutte le specializzazioni, ad eccezione body
di ; indica che il compilatore deve scegliere automaticamente una direttiva di generazione appropriata.
La dichiarazione
operation DoNothing() : Unit {
body ... { }
adjoint auto;
controlled auto;
controlled adjoint auto;
}
equivale a
operation DoNothing() : Unit
is Adj + Ctl { }
L'annotazione is Adj + Ctl
in questo esempio specifica le caratteristiche dell'operazione, che contengono le informazioni sui funtori supportati da una determinata operazione.
Per migliorare la leggibilità, è consigliabile annotare ogni operazione con una descrizione completa delle relative caratteristiche, ma il compilatore inserisce o completa automaticamente l'annotazione in base alle specializzazioni dichiarate in modo esplicito. Di contro, il compilatore genera anche specializzazioni che non sono state dichiarate in modo esplicito, ma che devono esistere in base alle caratteristiche annotate. Si dice che l'annotazione specificata ha dichiarato in modo implicito queste specializzazioni. Se possibile, il compilatore genera automaticamente le specializzazioni necessarie, selezionando una direttiva appropriata. Q# supporta quindi l'inferenza sia delle caratteristiche dell'operazione che delle specializzazioni esistenti basate su annotazioni (parziali) e specializzazioni definite in modo esplicito.
In un certo senso, le specializzazioni sono simili ai singoli overload per lo stesso oggetto chiamabile, con la differenza che possono essere previste determinate restrizioni in merito a quali overload è possibile dichiarare.