#julia
#julia
Вопрос:
Я пытаюсь понять ввод текста в Julia и сталкиваюсь со следующей проблемой Array
. Я написал функцию bloch_vector_2d(Array{Complex,2})
; подробная реализация не имеет значения. При вызове вот жалоба:
julia> bloch_vector_2d(rhoA)
ERROR: MethodError: no method matching bloch_vector_2d(::Array{Complex{Float64},2})
Closest candidates are:
bloch_vector_2d(::Array{Complex,2}) at REPL[56]:2
bloch_vector_2d(::StateAB) at REPL[54]:1
Stacktrace:
[1] top-level scope at REPL[64]:1
Проблема в том, что массив родительского типа не является автоматически родительским массивом дочернего типа.
julia> Complex{Float64} <: Complex
true
julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
Я думаю, что имело бы смысл наложить это на julia Array{Complex{Float64},2} <: Array{Complex,2}
. Или как правильно реализовать это в Julia? Любая помощь или комментарии приветствуются!
Ответ №1:
Этот вопрос подробно обсуждается в руководстве Julia здесь .
Цитирование соответствующей его части:
Другими словами, на языке теории типов параметры типа Julia являются инвариантными, а не ковариантными (или даже контравариантными). Это по практическим соображениям: хотя любой экземпляр
Point{Float64}
может концептуально быть похож на экземплярPoint{Real}
, оба типа имеют разные представления в памяти:
- Экземпляр
Point{Float64}
может быть компактно и эффективно представлен в виде непосредственной пары 64-разрядных значений;- Экземпляр
Point{Real}
должен быть способен содержать любую пару экземпляровReal
. Поскольку объекты, являющиеся экземплярами Real, могут иметь произвольный размер и структуру, на практике экземплярPoint{Real}
должен быть представлен в виде пары указателей на индивидуально выделенные реальные объекты.
Теперь, возвращаясь к вашему вопросу, как написать сигнатуру метода, тогда у вас есть:
julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Обратите внимание на разницу:
Array{<:Complex,2}
представляет объединение всех типов, которые являются 2D-массивами, eltype которых является подтипомComplex
(т. Е. Ни один массив не будет иметь этот точный тип).Array{Complex,2}
это тип, который может иметь массив, и этот тип означает, что вы можете хранитьComplex
в нем значения, которые могут иметь смешанный параметр.
Вот пример:
julia> x = Complex[im 1im;
1.0im Float16(1)im]
2×2 Array{Complex,2}:
im 0 1im
0.0 1.0im 0.0 1.0im
julia> typeof.(x)
2×2 Array{DataType,2}:
Complex{Bool} Complex{Int64}
Complex{Float64} Complex{Float16}
Также обратите внимание, что обозначение Array{<:Complex,2}
такое же, как и запись Array{T,2} where T<:Complex
(или более компактно Matrix{T} where T<:Complex
).
Ответ №2:
Это скорее комментарий, но я не могу не опубликовать его. Этот вопрос возникает так часто. Я расскажу вам, почему это явление должно возникнуть.
A Bag{Apple}
— это a Bag{Fruit}
, верно? Потому что, когда у меня есть a JuicePress{Fruit}
, я могу дать ему a Bag{Apple}
, чтобы сделать какой-то сок, потому Apple
что s — это Fruit
s.
Но теперь мы сталкиваемся с проблемой: на моей фабрике фруктовых соков, на которой я обрабатываю разные фрукты, произошел сбой. Я заказываю новый JuicePress{Fruit}
. Теперь, к сожалению, мне доставляют замену JuicePress{Lemon}
— но Lemon
s — это Fruit
s, так что, конечно, a JuicePress{Lemon}
— это a JuicePress{Fruit}
, верно?
Однако на следующий день я скармливаю яблоки новому прессу, и машина взрывается. Надеюсь, вы понимаете, почему: JuicePress{Lemon}
не является JuicePress{Fruit}
. Наоборот: a JuicePress{Fruit}
— это a JuicePress{Lemon}
— я могу давить лимоны прессом, не зависящим от фруктов! Они могли бы отправить мне a JuicePress{Plant}
, хотя, поскольку Fruit
s — это Plant
s.
Теперь мы можем получить более абстрактный. Реальная причина в том, что входные аргументы функции контравариантны, а выходные аргументы функции ковариантны (в идеализированной настройке)2. То есть, когда мы имеем
f : A -> B
тогда я могу передать супертипы A
и в итоге получить подтипы B
. Следовательно, когда мы фиксируем первый аргумент, индуцированная функция
(Tree -> Apple) <: (Tree -> Fruit)
всякий Apple <: Fruit
раз, когда — это ковариантный случай, он сохраняет направление <:
. Но когда мы исправим второй,
(Fruit -> Juice) <: (Apple -> Juice)
всякий Fruit >: Apple
раз, когда — это инвертирует направление <:
, и поэтому называется противоположным вариантом.
Это переносится на другие параметрические типы данных, поскольку там тоже обычно есть «выходные» параметры (как в Bag
) и «входные» параметры (как в JuicePress
). Также могут быть параметры, которые не ведут себя ни как (например, когда они встречаются в обоих вариантах) — тогда они называются инвариантными.
Теперь есть два способа, которыми языки с параметрическими типами решают эту проблему. На мой взгляд, более элегантным является пометка каждого параметра: отсутствие аннотации означает инвариантность,
-
означает ковариантность, означает контравариантность (это имеет технические причины — говорят, что эти параметры находятся в «положительном» и «отрицательном положении»). Итак, у нас был Bag[ T <: Fruit]
, или JuicePress[-T <: Fruit]
(должен быть синтаксис Scala, но я его не пробовал). Однако это усложняет подтипирование.
Другой путь — это то, что делает Julia (и, кстати, Java): все типы инвариантны 1, но вы можете указать верхний и нижний союзы на сайте вызова. Итак, вы должны сказать
makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
И вот как мы приходим к другим ответам.
1 За исключением кортежей, но это странно.
2 Эта терминология взята из теории категорий. Hom
-функтор контравариантен в первом и ковариантен во втором аргументе. Существует интуитивная реализация подтипа через «забывчивый» функтор из категории Typ
в набор Typ
элементов в <:
отношении. А терминология CT, в свою очередь, происходит от тензоров.
Комментарии:
1. Спасибо! Я думаю, что понимаю. Дело в том, что julia ориентирована на функциональность (нацелена на манипулирование стрелками в таких категориях, как JuicePress, а не только Bag). Действительно ли его дизайн мотивирован теорией категорий? Спасибо за редактирование моего вопроса тоже!
2. Нет, не мотивировано. Терминология взята оттуда, и она соответствует более широкой картине, но проблема возникает сама по себе в каждой системе с параметрическими типами и подтипами.
3. Я думаю, что было бы менее двусмысленно писать
(Tree -> Apple) <: (Tree -> Fruit)
. Мне потребовалась минута, чтобы прочитать эту строку.
Ответ №3:
Хотя обсуждение «как это работает» было сделано в другом ответе, лучшим способом реализации вашего метода является следующее:
function bloch_vector_2d(a::AbstractArray{Complex{T}}) where T<:Real
sum(a) 5*one(T) # returning something to see how this is working
end
Теперь это будет работать следующим образом:
julia> bloch_vector_2d(ones(Complex{Float64},4,3))
17.0 0.0im
Комментарии:
1. или просто
bloch_vector_2d(a::AbstractMatrix{<:Complex})
следуя предложению в моем ответе (я предполагаю, что @chau хочет ограничить подпись матрицами, заданными именем функции).