Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Q# admite operaciones y funciones con parámetros de tipo. Las bibliotecas estándar de Q# hacen un uso intensivo de los invocables de tipo parametrizado para proporcionar una serie de abstracciones útiles, incluidas funciones como Mapped
y Fold
que son familiares de los lenguajes funcionales.
Para motivar el concepto de parametrizaciones de tipo, considere el ejemplo de la función Mapped
, que aplica una función determinada a cada valor de una matriz y devuelve una nueva matriz con los valores calculados. Esta funcionalidad se puede describir perfectamente sin especificar los tipos de elemento de las matrices de entrada y salida. Dado que los tipos exactos no cambian la implementación de la función Mapped
, tiene sentido que debería ser posible definir esta implementación para tipos de elementos arbitrarios; queremos definir un de fábrica de o plantilla que, dados los tipos concretos de los elementos de la matriz de entrada y salida, devuelve la implementación de 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 varios parámetros de tipo que se pueden usar como tipos, o parte de los tipos, de la entrada o salida invocables, o ambos. Las excepciones son puntos de entrada, que deben ser concretos y no pueden ser parametrizados de tipo. Los nombres de parámetros de tipo comienzan con un tic (') y pueden aparecer varias veces en los tipos de entrada y salida. Todos los argumentos que corresponden al mismo parámetro de tipo de la firma invocable deben ser del mismo tipo.
Es necesario que se concretice un parámetro de tipo parametrizado, es decir, debe proporcionarse con los argumentos de tipo necesarios para poder asignarlos o pasarlos como argumento, de modo que todos los parámetros de tipo se puedan reemplazar por tipos concretos. Un tipo se considera concreto si es uno de los tipos integrados, un tipo struct
o si es concreto dentro del ámbito actual. En el ejemplo siguiente se muestra 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)) {
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
. Toma 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
) se establezca en true; esto se conoce a menudo como la versión controlada clásicamente 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 ejemplo, los parámetros de tipo se numeran para evitar que la discusión sea más confusa al proporcionar los parámetros de tipo en ambas funciones el mismo nombre. 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 controladas clásicamente de estas operaciones. La llamada de Mapped
resuelve su parámetro de tipo 'T1
para 'T3 => Unit
y su parámetro de tipo 'T2
para (Bool,'T3) => Unit
. El compilador deduce los argumentos de tipo de resolución en función del tipo del argumento especificado. Decimos que se implícitamente definidos 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. El CControlled<'T3>
de concretización explícita es necesario cuando no se pueden deducir los argumentos de tipo.
El tipo 'T3
es concreto en el contexto de AllCControlled
, ya que se conoce por cada invocación de AllCControlled
. Esto significa que tan pronto como se conoce el punto de entrada del programa (que no se puede parametrizar de tipo), por lo que es el tipo concreto 'T3
para cada llamada a AllCControlled
, de modo que se pueda generar una implementación adecuada para esa resolución de tipos en particular. Una vez conocido el punto de entrada a un programa, se pueden eliminar todos los usos de parámetros de tipo en tiempo de compilación. Nos referimos a este proceso como monomorfización.
Se necesitan algunas restricciones para asegurarse de que esto se puede hacer en tiempo de compilación, en lugar de 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));
}
Al ignorar que una invocación de Foo
da como resultado un bucle infinito, sirve para la ilustración.
Foo
se invoca con la versión controlada clásicamente de la operación original op
que se ha pasado, así como una tupla que contiene un bit clásico aleatorio además del argumento original.
Para cada iteración de la recursividad, el parámetro type 'TArg
de la siguiente llamada se resuelve en (Bool, 'TArg)
, donde 'TArg
es el parámetro type de la llamada actual. En concreto, supongamos que se invoca Foo
con la operación H
y un argumento arg
de tipo Qubit
.
Foo
luego se invoca con un argumento de tipo (Bool, Qubit)
, que luego invoca Foo
con un argumento de tipo (Bool, (Bool, Qubit))
, etc. Claramente, en este caso Foo
no se puede monomorfizar en tiempo de compilación.
Se aplican restricciones adicionales a los ciclos del gráfico de llamadas que implican solo los invocables de tipo parametrizado. Cada invocable debe invocarse con el mismo conjunto de argumentos de tipo después de atravesar el ciclo.
Nota:
Sería posible ser menos restrictivo y requerir que para cada invocable en el ciclo, hay un número finito de ciclos después del cual se invoca con el conjunto original de argumentos de tipo, como 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, en el caso de los ciclos que implican al menos un concreto invocable sin ningún parámetro de tipo, este tipo garantiza que los invocables parametrizados de tipo dentro de ese ciclo siempre se llamen con un conjunto fijo de argumentos de tipo.