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.
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.
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.