函数中 julia 中的抽象类型数组

cha*_*hau 4 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
Run Code Online (Sandbox Code Playgroud)

问题是父类型的数组不会自动成为子类型数组的父级。

julia> Complex{Float64} <: Complex
true

julia> Array{Complex{Float64},2} <: Array{Complex,2}
false
Run Code Online (Sandbox Code Playgroud)

我认为将这一点强加给 julia 是有道理的Array{Complex{Float64},2} <: Array{Complex,2}。或者在 Julia 中实现这一点的正确方法是什么?任何帮助或意见表示赞赏!

Bog*_*ski 6

此处的 Julia 手册中详细讨论了此问题。

引用其中的相关部分:

换句话说,用类型论的说法,Julia 的类型参数是不变的,而不是协变的(甚至是逆变的)。这是出于实际原因:虽然Point{Float64}从概念上讲,任何实例都可能类似于实例,但这Point{Real}两种类型在内存中具有不同的表示形式:

  • 的实例Point{Float64}可以简洁有效地表示为一对立即数 64 位值;
  • 的实例Point{Real}必须能够保存任何一对 的实例Real。由于作为 Real 实例的对象可以具有任意大小和结构,因此在实践中, 的实例Point{Real}必须表示为一对指向单独分配的 Real 对象的指针。

现在回到你的问题如何编写方法签名,那么你有:

julia> Array{Complex{Float64},2} <: Array{<:Complex,2}
true
Run Code Online (Sandbox Code Playgroud)

注意区别:

  • Array{<:Complex,2}表示其 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}
Run Code Online (Sandbox Code Playgroud)

另请注意,符号Array{<:Complex,2}与书写相同Array{T,2} where T<:Complex(或更紧凑Matrix{T} where T<:Complex)。


phi*_*ler 5

这更多是评论,但我可以毫不犹豫地发布它。这个问题很常见。我会告诉你为什么会出现这种现象。

ABag{Apple}是一个Bag{Fruit},对吧?因为,当我有 a 时JuicePress{Fruit},我可以给它 aBag{Apple}来制作一些果汁,因为Apples 是Fruits。

但是现在我们遇到了一个问题:我的果汁厂,我在其中加工不同的水果,失败了。我订购了一个新的JuicePress{Fruit}. 现在,不幸的是我得到了一个替代品JuicePress{Lemon}——但Lemons 是Fruits,所以 a 肯定JuicePress{Lemon}是 a JuicePress{Fruit},对吗?

然而,第二天,我将苹果喂给新印刷机,机器爆炸了。我希望你明白为什么:JuicePress{Lemon}不是一个JuicePress{Fruit}。恰恰相反:aJuicePress{Fruit}是 a JuicePress{Lemon}-- 我可以用与水果无关的压榨机压榨柠檬!JuicePress{Plant}不过,他们本可以寄给我的,因为Fruits 是Plants。

现在我们可以变得更抽象了。真正的原因是:函数输入参数是逆变的,而函数输出参数是协变的(在理想化设置中)2。也就是说,当我们有

f : A -> B
Run Code Online (Sandbox Code Playgroud)

然后我可以传入 的超类型A,并最终得到 的子类型B。因此,当我们修正第一个参数时,诱导函数

(Tree -> Apple) <: (Tree -> Fruit)
Run Code Online (Sandbox Code Playgroud)

每当Apple <: Fruit- 这是协变情况,它会保留 的方向<:。但是当我们修复第二个时

(Fruit -> Juice) <: (Apple -> Juice)
Run Code Online (Sandbox Code Playgroud)

每当Fruit >: Apple- 这颠倒了 的方向<:,因此被称为相反的变体

这会延续到其他参数数据类型,因为在那里,您通常也有“类似输出”的参数(如Bag)和“类似输入”的参数(如JuicePress)。也可能有参数的行为既不像(例如,当它们以两种方式出现时)——这些被称为invariant

现在有两种方法可以用参数类型的语言解决这个问题。在我看来,更优雅的方法是标记每个参数:没有注释意味着不变,+意味着协变,-意味着逆变(这有技术原因——据说这些参数出现在“正”和“负位置”)。所以我们有Bag[+T <: Fruit], 或JuicePress[-T <: Fruit]( 应该是 Scala 语法,但我还没有尝试过)。不过,这使得子类型化更加复杂。

要走的另一条路线是 Julia 所做的(以及顺便说一句,Java):所有类型都是不变的1,但您可以在调用站点指定上下联合。所以你必须说

makejuice(::JoicePress{>:T}, ::Bag{<:T}) where {T}
Run Code Online (Sandbox Code Playgroud)

这就是我们得出其他答案的方式。


1元组除外,但这很奇怪。

2这个术语来自范畴论。-Hom函子在第一个参数中是逆变的,在第二个参数中是协变的。通过“健忘”函子从类别Typ到关系Typ下的 es的偏序集,可以直观地实现子类型化<:。而 CT 术语反过来又来自张量