Opérations et fonctions

Comme précisé plus en détail dans la description du type de données qubit, les calculs quantiques sont exécutés sous forme d’effets secondaires des opérations qui sont prises en charge en mode natif sur le processeur quantique ciblé. Il s’agit en fait des seuls effets secondaires dans Q#. Comme tous les types sont immuables, aucun effet secondaire n’impacte une valeur explicitement représentée dans Q#. Par conséquent, tant qu’une implémentation d’un certain callable n’appelle pas directement ou indirectement l’une de ces opérations implémentées en mode natif, son exécution produit toujours la même sortie pour une même entrée.

Q# vous permet de répartir explicitement ces calculs purement déterministes en fonctions. Comme l’ensemble d’instructions prises en charge en mode natif n’est pas fixe ni intégré dans le langage lui-même, mais plutôt entièrement configurable et exprimé sous forme de bibliothèque Q#, le déterminisme est garanti en exigeant que les fonctions puissent appeler uniquement d’autres fonctions, et pas des opérations. Par ailleurs, les instructions natives qui ne sont pas déterministes, parce qu’elles impactent l’état quantique, sont représentées sous forme d’opérations. Avec ces deux restrictions, les fonctions peuvent être évaluées dès que leur valeur d’entrée est connue et, en principe, n’ont jamais besoin d’être évaluées plusieurs fois pour la même entrée.

Par conséquent, Q# fait la distinction entre deux types de callables : les opérations et les fonctions. Tous les callables prennent un seul argument (éventuellement à valeur de tuple) comme entrée et produisent une seule valeur (tuple) comme sortie. Syntaxiquement, le type d’opération est exprimé sous la forme <TIn> => <TOut> is <Char>, où <TIn> doit être remplacé par le type d’argument, <TOut> par le type de retour et <Char> par les caractéristiques de l’opération. Si aucune caractéristique n’a besoin d’être spécifiée, la syntaxe est simplifiée en <TIn> => <TOut> . De même, les types de fonction sont exprimés sous la forme <TIn> -> <TOut>.

À part cette garantie de déterminisme, il y a peu de différence entre les opérations et les fonctions. Les deux sont des valeurs de première classe qui peuvent être transmises librement. Elles peuvent être utilisées comme valeurs de retour ou arguments pour d’autres callables, comme illustré dans l’exemple suivant :

function Pow<'T>(op : 'T => Unit, pow : Int) : 'T => Unit {
    return PowImpl(op, pow, _);
}

Elles peuvent être toutes les deux instanciées sur la base d’une définition paramétrisée de type, par exemple, la fonction paramétrisée de typePow ci-dessus, et elles peuvent être partiellement appliquées, comme c’est le cas dans l’instruction return de l’exemple.

Caractéristiques de l’opération

En plus des informations sur le type d’entrée et de sortie, le type d’opération contient des informations sur les caractéristiques d’une opération. Ces informations, par exemple, décrivent les foncteurs pris en charge par l’opération. Par ailleurs, la représentation interne contient également des informations pertinentes sur l’optimisation, déduites par le compilateur.

Les caractéristiques d’une opération sont un ensemble d’étiquettes prédéfinies et intégrées. Elles sont exprimées sous forme d’expression spéciale qui fait partie de la signature de type. L’expression se compose d’un des ensembles d’étiquettes prédéfinis ou d’une combinaison d’expressions de caractéristiques via un opérateur binaire pris en charge.

Il existe deux ensembles prédéfinis, Adj et Ctl.

  • Adj est l’ensemble qui contient une seule étiquette indiquant qu’une opération est adjoignable, ce qui signifie qu’elle prend en charge le foncteur Adjoint et que la transformation quantique appliquée peut être « annulée », c’est-à-dire inversée.
  • Ctl est l’ensemble qui contient une seule étiquette indiquant qu’une opération est contrôlable, ce qui signifie qu’elle prend en charge le foncteur Controlled et que son exécution peut être conditionnée par l’état d’autres qubits.

Les deux opérateurs pris en charge dans le cadre des expressions de caractéristiques sont l’union ensembliste + et l’intersection ensembliste *. Dans EBNF,

    predefined = "Adj" | "Ctl";
    characteristics = predefined 
        | "(", characteristics, ")" 
        | characteristics ("+"|"*") characteristics;

Comme on peut s’y attendre, * a une priorité plus élevée que + et les deux sont associatifs à gauche. Le type d’une opération unitaire, par exemple, est exprimé de la forme <TIn> => <TOut> is Adj + Ctl, où <TIn> doit être remplacé par le type de l’argument de l’opération, et <TOut> remplacé par le type de la valeur retournée.

Notes

L’indication des caractéristiques d’une opération sous cette forme présente deux avantages majeurs : tout d’abord, de nouvelles étiquettes peuvent être introduites sans avoir un nombre exponentiel de mots clés de langage pour toutes les combinaisons d’étiquettes. D’autre part, l’utilisation d’expressions pour indiquer les caractéristiques d’une opération prend également en charge les paramétrisations sur les caractéristiques d’opération à l’avenir.