Поделиться через


Параметризации типов

Q# поддерживает операции и функции с параметризацией типов. В стандартных библиотеках Q# вызываемые объекты с параметризацией типов широко применяются для предоставления множества полезных абстракций, включая такие функции, как Mapped и Fold, знакомые по функциональным языкам.

Чтобы пояснить принцип работы параметризации типа, рассмотрим в качестве примера функцию Mapped, которая применяет заданную функцию к каждому значению в массиве и возвращает новый массив с вычисленными значениями. Такую функциональность можно легко описать без указания типов элементов входного и выходного массивов. Так как конкретные типы не должны влиять на реализацию функции Mapped, имеет смысл определить эту реализацию для произвольных типов элементов. Нам нужно определить фабрику или шаблон, которые возвращают соответствующую реализацию функции с учетом типов элементов во входном и выходном массивах. Формально это выражается в виде параметров типов.

Конкретизация

В объявлении любой операции или функции можно указать один или несколько параметров типов, которые могут использоваться в качестве типов или части типов входных и (или) выходных данных вызываемого объекта. Исключением являются точки входа, которые должны иметь конкретные, не параметризованные типы. Имена параметров типов начинаются с машинописного апострофа (') и могут встречаться несколько раз в списке входных и выходных типов. Все аргументы, соответствующие одному и тому же параметру типа в сигнатуре вызываемого объекта, должны иметь один и тот же тип.

Вызываемый объект с параметризацией типов должен быть конкретизирован (т. е. должен получить требуемые аргументы типов), прежде чем его можно будет назначить или передать в качестве аргумента. Это необходимо для того, чтобы все параметры типов можно было заменить конкретными типами. Тип считается конкретным, если это один из встроенных типов, пользовательский тип либо если он является конкретным в текущей области. В приведенном ниже примере показано, что понимается под типом, конкретным в пределах текущей области. После примера даются пояснения.

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

Функция CControlled определена в пространстве имен Microsoft.Quantum.Canon. Она принимает операцию op типа 'TIn => Unit в качестве аргумента и возвращает новую операцию типа (Bool, 'TIn) => Unit, которая применяет исходную операцию при условии, что классический бит (типа Bool) имеет значение true. Такую реализацию часто называют классически контролируемой версией op.

Функция Mapped принимает в качестве аргумента массив элементов произвольного типа 'T1, применяет заданную функцию mapper к каждому элементу и возвращает новый массив типа 'T2[], содержащий сопоставленные элементы. Она определена в пространстве имен Microsoft.Quantum.Array. В этом примере параметры типов пронумерованы, чтобы их имена в двух функциях не совпадали. Это нужно лишь для того, чтобы упростить пояснения. На самом деле имена параметров типов в разных вызываемых объектах могут совпадать, и выбранное имя является видимым и имеет значение только в пределах определения данного вызываемого объекта.

Функция AllCControlled принимает массив операций и возвращает новый массив с классически контролируемыми версиями этих операций. При вызове функции Mapped ее параметр типа 'T1 разрешается в 'T3 => Unit, а параметр типа 'T2 — в (Bool,'T3) => Unit. Аргументы типов выводятся компилятором на основе типа заданного аргумента. Мы говорим, что они неявно определяются аргументом выражения вызова. Аргументы типов также можно указать явным образом, как это сделано для CControlled в той же строке. Явная конкретизация CControlled<'T3> необходима в том случае, если аргументы типов невозможно вывести.

Тип 'T3 является конкретным в контексте функции AllCControlled, так как он известен для каждого вызоваAllCControlled. Это означает, что как только становится известна точка входа в программу, которая не допускает параметризации типов, становится известен и конкретный тип 'T3 для каждого вызова AllCControlled, что позволяет создать подходящую реализацию для разрешения этого типа. Как только точка входа в программу становится известна, все случаи использования параметров типов можно исключить во время компиляции. Этот процесс называется мономорфизацией.

Чтобы это было возможно во время компиляции, а не только во время выполнения, требуется наложить некоторые ограничения.

Ограничения

Рассмотрим следующий пример.

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

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

Не обращайте внимание на то, что вызов Foo приведет к бесконечному циклу. Данный пример служит лишь для иллюстрации. Операция Foo вызывает саму себя с помощью классически контролируемой версии исходной операции op, которая была передана вместе с кортежем, содержащим случайный классический бит в дополнение к исходному аргументу.

При каждой итерации рекурсии параметр типа 'TArg следующего вызова разрешается в (Bool, 'TArg), где 'TArg — это параметр типа в текущем вызове. Для конкретности предположим, что Foo вызывается с операцией H и аргументом arg типа Qubit. Затем операция Foo вызовет себя с аргументом типа (Bool, Qubit), который далее вызовет операцию Foo с аргументом типа (Bool, (Bool, Qubit)) и так далее. Очевидно, в этом случае операцию Foo невозможно мономорфизировать во время компиляции.

К циклам в графе вызовов, в котором задействованы только вызываемые типы с параметризацией типов, применяются дополнительные ограничения. Каждый вызываемый объект должен вызываться с одним и тем же набором аргументов типов после завершения цикла.

Примечание

Реализацию можно было бы сделать менее строгой, ограничив количество циклов для каждого вызываемого объекта, после которого объект вызывается с исходным набором аргументов типов, как в случае следующей функции:

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

Однако для простоты применяется более строгое требование. Обратите внимание, что в циклах, в которых используется по крайней мере один конкретный вызываемый объект без параметров типов, этот объект обеспечивает вызов других объектов с параметризацией типов в рамках этого цикла с фиксированным набором аргументов типов.