Parametrizaciones de tipos

Q# soporta operaciones y funciones con parámetros de tipo. Las bibliotecas estándar de Q# hacen un uso intensivo de invocables parametrizados por tipo para proporcionar una serie de abstracciones útiles, entre las que se incluyen funciones como Mapped y Fold, que son familiares en los lenguajes funcionales.

Para motivar el concepto de parametrización de tipos, considere el ejemplo de la función Mapped, que aplica una función dada a cada valor de un array y devuelve una nueva matriz con los valores procesados. Esta funcionalidad se puede describir perfectamente sin especificar los tipos de los elementos de las matrices de entrada y de salida. Puesto que los tipos exactos no cambian la implementación de la función Mapped, tiene sentido que sea posible definir esta implementación para tipos de elementos arbitrarios; queremos definir una factoría o plantilla que, teniendo en cuenta los tipos específicos de los elementos de la matriz de entrada y de salida, devuelva la implementación de la función correspondiente. Esta noción se formaliza en forma de parámetros de tipo.

Concretización

Cualquier declaración de operación o función puede especificar uno o más parámetros de tipo que se pueden utilizar como los tipos (o parte de los tipos) de la entrada o salida del invocable, o ambos. La excepción son los puntos de entrada, que se deben concretar y no se pueden parametrizar por tipo. Los nombres de los parámetros de tipo comienzan con una tilde (') y pueden aparecer varias veces en los tipos de entrada y salida. Todos los argumentos que correspondan al mismo parámetro de tipo en la signatura de llamada tienen que ser del mismo tipo.

Un invocable parametrizado por tipo se debe concretar, es decir, se deben proporcionar los argumentos de tipo necesarios antes de que se pueda asignar o pasar como argumento, de forma que todos los parámetros de tipo se puedan sustituir por tipos concretos. Se considera que un tipo es concreto si es uno de los tipos integrados, un tipo definido por el usuario o si es concreto dentro del ámbito actual. El siguiente ejemplo ilustra lo que significa que un tipo sea concreto dentro del ámbito actual, y se explica con más detalle a continuación:

    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 función CControlled se define en el espacio de nombres Microsoft.Quantum.Canon. Acepta una operación op de tipo 'TIn => Unit como argumento y devuelve una nueva operación de tipo (Bool, 'TIn) => Unit que aplica la operación original siempre que un bit clásico (de tipo Bool) esté establecido en true; se suele hacer referencia a esto como la versión controlada de forma clásica de op.

La función Mapped toma una matriz de un tipo de elemento arbitrario 'T1 como argumento, aplica la función mapper dada a cada elemento y devuelve una nueva matriz de tipo 'T2[] que contiene los elementos asignados. Se define en el espacio de nombres Microsoft.Quantum.Array Para el propósito del ejemplo, los parámetros de tipo están numerados para evitar hacer la discusión más confusa dando a los parámetros de tipo en ambas funciones el mismo nombre. Sin embargo, esto no es necesario; los parámetros de tipo para diferentes invocables pueden tener el mismo nombre, y el nombre elegido solo es visible y relevante dentro de la definición de ese invocable.

La función AllCControlled toma una matriz de operaciones y devuelve una nueva matriz que contiene las versiones clásicas controladas de estas operaciones. La llamada Mapped resuelve su parámetro de tipo 'T1 a 'T3 => Unit, y su parámetro de tipo 'T2 a (Bool,'T3) => Unit. Los argumentos de tipo de resolución son inferidos por el compilador basándose en el tipo del argumento dado. e que están definidos implícitamente por el argumento de la expresión de llamada. Los argumentos de tipo también se pueden especificar explícitamente, como se hace para CControlled en la misma línea. La concreción explícita CControlled<'T3> es necesaria cuando los argumentos de tipo no se pueden inferir.

El tipo 'T3 es concreto dentro del contexto de AllCControlled, ya que se conoce para cada invocación de AllCControlled. Esto significa que tan pronto como es conocido el punto de entrada del programa (que no puede ser parametrizado por tipo), también lo es el tipo concreto 'T3 para cada llamada a AllCControlled, de tal manera que se puede generar una implementación adecuada para la resolución de ese tipo particular. Una vez que el punto de entrada a un programa es conocido, todos los usos de los parámetros de tipo se pueden eliminar en tiempo de compilación. Este proceso se conoce como monomorfización.

Son necesarias algunas restricciones para garantizar que esto pueda hacerse en tiempo de compilación y no solo en tiempo de ejecución.

Restricciones

Considere el ejemplo siguiente:

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

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

Un ejemplo ilustrativo sería ignorar que una invocación de Foo dará lugar a un bucle infinito. Foo se invoca a sí mismo con la versión controlada de forma clásica de la operación original op que se le haya pasado, así como con una tupla que contenga un bit clásico aleatorio además del argumento original.

Para cada iteración en la recursión, el parámetro de tipo 'TArg de la siguiente llamada se resuelve como (Bool, 'TArg), donde 'TArg es el parámetro de tipo de la llamada actual. Concretamente, supongamos que Foo se invoca con la operación H y un argumento arg de Qubit. Entonces Foo se invocará a sí mismo con un argumento de tipo (Bool, Qubit), que a su vez invocará a Foo con un argumento de tipo (Bool, (Bool, Qubit)), y así sucesivamente. Es evidente que en este caso Foo no se puede monomorfizar en tiempo de compilación.

Se aplican restricciones adicionales a los ciclos en el gráfico de llamadas que implican solo invocables parametrizados por tipo. Cada llamada se debe invocar con el mismo conjunto de argumentos de tipo después de atravesar el ciclo.

Nota

Se podría ser menos restrictivo y requerir que para cada invocable del ciclo haya un número finito de ciclos después de los cuales se invoque con el conjunto original de argumentos de tipo, como es el caso de la siguiente función:

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

Para simplificar, se aplica el requisito más restrictivo. Tenga en cuenta que para los ciclos que implican al menos un invocable concreto sin ningún parámetro de tipo, dicho invocable garantizará que se llame siempre a los invocables parametrizados por tipo de ese ciclo con un conjunto fijo de argumentos de tipo.