Создание подтипов и вариантность
Q# поддерживает только некоторые механизмы преобразования. Неявные преобразования могут происходить только при применении бинарных операторов, при вычислении условных выражений и при построении литерала массива. В таких случаях определяется общий супертип, и необходимые преобразования выполняются автоматически. Помимо таких неявных преобразований, возможно и часто необходимо явное преобразование посредством вызова функций.
В настоящее время единственное существующее отношение подтипизации относится к операциям. Представляется само собой разумеющимся, что должна существовать возможность замены операции, которая поддерживает больше функторов, чем необходимо. В частности, для любых двух конкретных типов TIn
и TOut
отношение подтипизации имеет следующий вид:
(TIn => TOut) :>
(TIn => TOut is Adj), (TIn => TOut is Ctl) :>
(TIn => TOut is Adj + Ctl)
Здесь A :> B
указывает на то, что B
является подтипом A
. Иначе говоря, тип B
является более строгим по сравнению с A
, то есть значение типа B
можно использовать везде, где требуется значение типа A
. Если вызываемому объекту требуется аргумент (элемент) типа A
, его можно спокойно заменить аргументом типа B
, так как он обладает всеми необходимыми возможностями.
Подобный полиморфизм касается и кортежей в том смысле, что тип кортежа B
является подтипом для типа кортежа A
, если он содержит то же число элементов и тип каждого его элемента является подтипом для типа соответствующего элемента в A
. Это называется подтипизацией в глубину. В настоящее время подтипизация в ширину не поддерживается, то есть отношение подтипизации между любыми двумя пользовательскими типами или пользовательским типом и любым встроенным типом невозможно. Это связано с наличием оператора unwrap
, который позволяет извлечь содержимое кортежа, включая все именованные и анонимные элементы.
Примечание
Что касается вызываемых объектов, если такой объект обрабатывает аргумент типа A
, он также может обработать аргумент типа B
. Если вызываемый объект передается в качестве аргумента в другой вызываемый объект, он должен иметь возможность обрабатывать все, что требуется согласно сигнатуре типа. Это означает, что если вызываемому объекту требуется возможность обработки аргумента типа B
, можно безопасно передать любой вызываемый объект, который способен обработать более общий аргумент типа A
. И наоборот, предполагается, что если передаваемый вызываемый объект должен возвращать значение типа A
, возможности возврата значения типа B
достаточно, так как это значение обеспечивает все необходимые возможности.
Тип операции или функции является контравариантным по типу аргумента и ковариантным по типу возвращаемого значения. Поэтому из A :> B
следует, что для любого конкретного типа T1
верно следующее:
(B → T1) :> (A → T1), and
(T1 → A) :> (T1 → B)
Здесь →
может быть функцией или операцией, а аннотации с характеристиками опущены.
Подставив вместо A
соответственно (B → T2)
и (T2 → A)
, а вместо — B
соответственно (A → T2)
и (T2 → B)
, мы приходим к заключению, что для любого конкретного типа T2
верно следующее:
((A → T2) → T1) :> ((B → T2) → T1), and
((T2 → B) → T1) :> ((T2 → A) → T1), and
(T1 → (B → T2)) :> (T1 → (A → T2)), and
(T1 → (T2 → A)) :> (T1 → (T2 → B))
Согласно индукции каждое дополнительное косвенное обращение изменяет вариантность типа аргумента, а вариантность типа возвращаемого значения остается без изменений.
Примечание
Из этого также очевидно, каким должно быть поведение вариантности массивов. Получение элементов посредством оператора доступа к элементу соответствует вызову функции типа (Int -> TItem)
, где TItem
— это тип элементов массива. Поскольку эта функция передается неявным образом при передаче массива, массивы должны быть ковариантными по типу элементов. То же самое верно в отношении кортежей, которые являются неизменяемыми и поэтому ковариантны относительно типа каждого элемента.
Если бы массивы не были неизменяемыми, существование конструкции, позволяющей задавать элементы массива и, таким образом, принимающей аргумент типа TItem
, подразумевало бы контравариантность массивов. По этой причине типы данных, поддерживающие получение и задание элементов, могут быть только инвариантными, то есть отношение подтипизации полностью отсутствует. B[]
не является подтипом A[]
, даже если B
— подтип A
. Хотя массивы в Q# являются неизменяемыми, они инвариантны, а не ковариантны. Это означает, например, что значение типа (Qubit => Unit is Adj)[]
нельзя передать в вызываемый объект, требующий аргумента типа (Qubit => Unit)[]
.
Инвариантность массивов обеспечивает большую гибкость работы с ними и их оптимизации во время выполнения, но в будущем подход может измениться.