Массивы абстрактного типа в julia в функциях

#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 раз, когда — это инвертирует направление <: , и поэтому называется contra variant .

Это переносится на другие параметрические типы данных, поскольку там тоже обычно есть «выходные» параметры (как в 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 хочет ограничить подпись матрицами, заданными именем функции).