Partager via


Paramétrages de types

Q# prend en charge les opérations et les fonctions paramétrisées en fonction du type. Les bibliothèques standard Q# utilisent de manière intensive les callables paramétrés en type pour fournir une série d’abstractions utiles, notamment des fonctions telles que Mapped et Fold, que vous avez découvertes dans les langages fonctionnels.

Pour motiver le concept de paramétrage de type, considérez l’exemple de la fonction Mapped, qui applique une fonction donnée à chaque valeur d’un tableau et retourne un nouveau tableau avec les valeurs calculées. Cette fonctionnalité peut être parfaitement décrite sans spécifier les types d’éléments des tableaux d’entrée et de sortie. Comme les types exacts ne modifient pas l’implémentation de la fonction Mapped, il est logique qu’il soit possible de définir cette implémentation pour les types d’éléments arbitraires. Nous souhaitons définir une fabrique ou un modèle qui, selon les types concrets des éléments des tableaux d’entrée et de sortie, retourne l’implémentation de fonction correspondante. Cette notion est formalisée sous la forme de paramètres de type.

Concrétisation

Toute déclaration d’opération ou de fonction peut spécifier un ou plusieurs paramètres de type qui peuvent être utilisés comme types, ou partie des types, de l’entrée et/ou de la sortie du callable. Les exceptions sont les points d’entrée, qui doivent être concrets et ne peuvent pas être paramétrés en type. Les noms de paramètre de type commencent par une apostrophe (') et peuvent apparaître plusieurs fois dans les types d’entrée et de sortie. Tous les arguments qui correspondent au même paramètre de type dans la signature du callable doivent être du même type.

Un callable paramétré en type doit être concrétisé, c’est-à-dire qu’il doit être fourni avec les arguments de type nécessaires, avant de pouvoir être assigné ou transmis comme argument, de sorte que tous les paramètres de type peuvent être remplacés par des types concrets. Un type est considéré comme concret s’il s’agit de l’un des types intégrés, d’un type défini par l’utilisateur ou s’il est concret dans l’étendue actuelle. L’exemple suivant illustre ce que cela signifie pour qu’un type soit concret dans la portée actuelle, et est expliqué plus en détail ci-dessous :

    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); 
    }

La fonction CControlled est définie dans l'espace de noms Microsoft.Quantum.Canon. Elle prend une opération op de type 'TIn => Unit comme argument et retourne une nouvelle opération de type (Bool, 'TIn) => Unit qui applique l’opération d’origine, à condition qu’un bit classique (de type Bool) ait la valeur true ; cela est souvent appelé la version contrôlée de manière classique de op.

La fonction Mapped prend un tableau d’un type d’élément arbitraire 'T1 comme argument, applique la fonction mapper donnée à chaque élément et retourne un nouveau tableau de type 'T2[] contenant les éléments mappés. Elle est définie dans l’espace de noms Microsoft.Quantum.Array. Dans le cadre de l’exemple, les paramètres de type sont numérotés pour éviter de rendre la discussion plus confuse en donnant au même nom les paramètres de type dans les deux fonctions. Cela n’est pas nécessaire. les paramètres de type de différents callables peuvent avoir le même nom et le nom choisi est visible uniquement et pertinent dans la définition de ce callable.

La fonction AllCControlled prend un tableau d’opérations et retourne un nouveau tableau contenant les versions de ces opérations contrôlées de façon classique. L’appel de Mapped résout son paramètre de type 'T1 en 'T3 => Unit et son paramètre de type 'T2 en (Bool,'T3) => Unit. La résolution des arguments de type est déduite par le compilateur en fonction du type de l’argument donné. Nous disons qu’ils sont implicitement définis par l’argument de l’expression d’appel. Les arguments de type peuvent également être spécifiés explicitement, comme c’est le cas pour CControlled dans la même ligne. La concrétisation CControlled<'T3> explicite est nécessaire lorsque les arguments de type ne peuvent pas être déduits.

Le type 'T3 est concret dans le contexte de AllCControlled, car il est connu pour chaque appel de AllCControlled. Cela signifie que, dès que le point d’entrée du programme, qui ne peut pas être paramétré en type, est connu, le type concret 'T3 pour chaque appel à AllCControlled l’est également, de sorte qu’une implémentation appropriée pour cette résolution de type particulière peut être générée. Une fois que le point d’entrée d’un programme est connu, toutes les utilisations des paramètres de type peuvent être éliminées au moment de la compilation. Ce processus est appelé monomorphisation.

Certaines restrictions sont nécessaires pour s’assurer que cette opération peut effectivement être effectuée au moment de la compilation et pas seulement au moment de l’exécution.

Restrictions

Prenons l’exemple suivant :

    operation Foo<'TArg> (
        op : 'TArg => Unit,
        arg : 'TArg
    ) : Unit {

        let cbit = RandomInt(2) == 0;
        Foo(CControlled(op), (cbit, arg));        
    } 

En ignorant qu’un appel de Foo générera une boucle infinie, il répond à l’objectif de l’illustration. Foo s’appelle lui-même avec la version contrôlée de manière classique de l’opération op d’origine qui a été transmise, ainsi qu’un tuple contenant un bit classique aléatoire en plus de l’argument d’origine.

Pour chaque itération dans la récursivité, le paramètre de type 'TArg de l’appel suivant est résolu en (Bool, 'TArg), où 'TArg est le paramètre de type de l’appel en cours. Concrètement, supposons que Foo est appelé avec l’opération H et un argument arg de type Qubit. Foo s’appellera ensuite lui-même avec un argument de type (Bool, Qubit), qui appellera ensuite Foo avec un argument de type (Bool, (Bool, Qubit)), et ainsi de suite. En clair, dans ce cas Foo ne peut pas être monomorphe au moment de la compilation.

Des restrictions supplémentaires s’appliquent aux cycles dans le graphique des appels qui impliquent uniquement les callables paramétrés en type. Chaque callable doit être appelé avec le même jeu d’arguments de type après avoir parcouru le cycle.

Notes

Il serait possible d’être moins restrictif et de demander que pour chaque callable dans le cycle, il existe un nombre fini de cycles après lesquels il est appelé avec le jeu d’arguments de type d’origine, comme dans le cas de la fonction suivante :

   function Bar<'T1,'T2,'T3>(a1:'T1, a2:'T2, a3:'T3) : Unit{
       Bar<'T2,'T3,'T1>(a2, a3, a1);
   }

Par souci de simplicité, l’exigence la plus restrictive est appliquée. Notez que pour les cycles qui impliquent au moins un callable concret sans aucun paramètre de type, un tel callable garantit que les callables paramétrés en type dans ce cycle seront toujours appelés avec un ensemble fixe d’arguments de type.