Engee 文档

有关类型的其他信息

如果您已经在使用Julia,那么您了解类型所起的基本作用。 在这里,我们将尝试更详细地考虑这个问题,特别注意 参数类型

类型和集合(Any`和`Union{}/Bottom

也许考虑Julia类型系统的最简单方法是根据集合。 当程序使用单个值时,类型指的是一组值。 这和收藏不一样。 例如,集(`Set')的值本身就是一个单一的’Set’值。 相反,一个类型描述了一组可能的值,表达了我们所拥有的价值的不确定性。

_concret_类型’T’描述一组值,其直接标签由函数返回 `typeof',看起来像一个’T'。 抽象类型描述了一些可能的较大值集。

Any'描述可能值的总和。 'Integer'是包含’Int`的’Any’的子集, 'Int8'等特定类型。 在内部,Julia也积极使用另一种类型,称为’Bottom,也可以写成’Union{}'。 它对应于一个空集。

Julia类型支持标准集合论操作:您可以询问`T1`是否是`t2`的’子集'(子类型),带有`T1<:T2`。 以同样的方式,您将两种类型与函数相交 'typeintersect',使用类型取他们的联合 'Union'并使用函数计算包含其联合的类型 '打字':

julia> typeintersect(Int, Float64)
Union{}

julia> Union{Int, Float64}
Union{Float64, Int64}

julia> typejoin(Int, Float64)
Real

julia> typeintersect(Signed, Union{UInt8, Int8})
Int8

julia> Union{Signed, Union{UInt8, Int8}}
Union{UInt8, Signed}

julia> typejoin(Signed, Union{UInt8, Int8})
Integer

julia> typeintersect(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Int64, Float64}

julia> Union{Tuple{Integer, Float64}, Tuple{Int, Real}}
Union{Tuple{Int64, Real}, Tuple{Integer, Float64}}

julia> typejoin(Tuple{Integer, Float64}, Tuple{Int, Real})
Tuple{Integer, Real}

虽然这些操作看起来很抽象,但它们是Julia的核心。 例如,方法调度是通过逐步遍历方法列表中的元素来执行的,直到达到元组类型的参数是方法签名的子类型的元素。 为了使该算法起作用,重要的是方法按其特异性排序,搜索从最具体的方法开始。 因此,Julia也实现了部分类型顺序。 这使用类似于`<:`的功能,但有以下将讨论的差异。

UnionAll的类型

Julia类型系统还可以表示可迭代的类型联合:某个变量的所有值上的类型联合。 这对于描述某些参数值未知的参数类型是必要的。

例如,类型 'Array'有两个参数,如’Array{Int,2}`. 如果我们不知道元素类型,我们可以写’数组{T,2} 其中T',这是’数组的联合{T,2}'对于`T`的所有值:'+Union{Array{Int8,2},阵列{Int16,2}, …​}+`.

此类型由一个`unionAll`对象表示,该对象包含一个变量(在本例中为`T',类型为’TypeVar')和一个包装类型(在本例中为’Array{T,2}`).

考虑以下方法。

f1(A::Array) = 1
f2(A::Array{Int}) = 2
f3(A::Array{T}) where {T<:Any} = 3
f4(A::Array{Any}) = 4

签名(如部分所述 函数调用)f3’是一个`unionAll’类型,封装了tuple类型:'+Tuple{typeof(f3), Array{T}}其中T+。 除了`f4’之外,所有的都可以使用`a=[1,2]`来调用。 除了`f2’之外的所有内容都可以使用`b=Any[1,2]`调用。

让我们更详细地看看这些类型。

julia> dump(Array)
UnionAll
  var: TypeVar
    name: Symbol T
    lb: Union{}
    ub: Any
  body: UnionAll
    var: TypeVar
      name: Symbol N
      lb: Union{}
      ub: Any
    body: Array{T, N} <: DenseArray{T, N}
      ref::MemoryRef{T}
      size::NTuple{N, Int64}

这表明’Array’实际上将类型命名为’unionAll'。 每个参数都有一个嵌套类型’unionAll'。 '数组’语法{Int,2}'等价于'数组{Int}{2}`. 在内部,每个`unionAll`类型的实例都是用一个特定的变量值创建的,一次一个,从最外层的一个开始。 在这种情况下,省略最终类型参数具有自然的含义。 '数组{Int}'输出与`Array等效的类型{Int,N} 在哪里。

'TypeVar’本身不是一个类型,而是应该被视为`unionAll`类型结构的一部分。 类型变量的值有下限和上限(在字段"lb"和"ub"中)。 符号名称"名称"纯粹是装饰性的。 在内部’TypeVar’类型通过地址进行比较,因此它们被定义为可变的,以便可以区分"不同"类型的变量。 但是,按照惯例,它们不应该是可变的。

"TypeVar"类型可以手动构建:

julia> TypeVar(:V, Signed, Real)
Signed<:V<:Real

有一些方便的版本允许您省略除`name`字符之外的任何这些参数。

'数组的语法{T} 其中T<:Integer'降低到以下:

let T = TypeVar(:T,Integer)
    UnionAll(T, Array{T})
end

因此,'TypeVar’很少需要手动创建(而且,应该避免)。

</no-翻译>

自由变量

任何类型的变量的概念在类型系统中是非常重要的。 我们说变量’V’具有自由类型`T’如果’T’不包含类型’unionAll',它表示变量’V'。 例如,类型'数组{Array{V} 其中V<:Integer}'没有自由变量,而是'Array的一部分{V}'里面有一个自由变量’V'。

从某种意义上说,具有自由变量的类型根本不是类型。 考虑类型'数组{Array{T}}其中T,指数组的所有同质数组。 看起来内部类型是'Array{T}本身认为的'是指任何类型的数组。 但是,外部数组的每个元素都必须具有_this_array类型,因此Array{T}'不能引用任何旧数组。 我们可以说'数组{T}'实际上"发生"了几次,并且’T`每"时间"都应该具有相同的值。

为此,C API中的`jl_has_free_typevars’函数非常重要。 它返回true的类型不会在子类型和其他类型函数中给出可理解的答案。

打字名称

以下两种类型 'Array'在功能上是等效的,但以不同的方式输出:

julia> TV, NV = TypeVar(:T), TypeVar(:N)
(T, N)

julia> Array
Array

julia> Array{TV, NV}
Array{T, N}

它们可以通过检查类型`name`字段来区分,该字段是类型`TypeName`的对象。:

julia> dump(Array{Int,1}.name)
TypeName
  name: Symbol Array
  module: Module Core
  names: empty SimpleVector
  wrapper: UnionAll
    var: TypeVar
      name: Symbol T
      lb: Union{}
      ub: Any
    body: UnionAll
      var: TypeVar
        name: Symbol N
        lb: Union{}
        ub: Any
      body: Array{T, N} <: DenseArray{T, N}
  cache: SimpleVector
    ...

  linearcache: SimpleVector
    ...

  hash: Int64 -7900426068641098781
  mt: MethodTable
    name: Symbol Array
    defs: Nothing nothing
    cache: Nothing nothing
    max_args: Int64 0
    module: Module Core
    : Int64 0
    : Int64 0

在这种情况下,相应的字段是’wrapper`,它包含对用于创建新类型’Array’的顶级类型的引用。

julia> pointer_from_objref(Array)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array.body.body.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

julia> pointer_from_objref(Array{TV,NV})
Ptr{Cvoid} @0x00007fcc80c4d930

julia> pointer_from_objref(Array{TV,NV}.name.wrapper)
Ptr{Cvoid} @0x00007fcc7de64850

中的"包装器"字段 'Array'指向自身,但对于’Array{TV,NV}'它指向原始类型定义。

其他领域呢? hash为每种类型分配一个整数值。 要研究"缓存"字段,建议选择比Array更少使用的类型。 首先,让我们创建自己的类型:

julia> struct MyType{T,N} end

julia> MyType{Int,2}
MyType{Int64, 2}

julia> MyType{Float32, 5}
MyType{Float32, 5}

创建参数类型的实例时,每个特定类型都存储在类型缓存’MyType.body.body.name.cache`)中。但是,不缓存包含自由类型变量的实例。

元组的类型

元组类型是一个有趣的特例。 为了使用像`x::Tuple’这样的声明进行调度,类型必须能够容纳任何元组。 让我们检查参数:

julia> Tuple
Tuple

julia> Tuple.parameters
svec(Vararg{Any})

与其他类型不同,元组类型在其参数中是协变的,因此此定义允许"元组"匹配任何元组类型。:

julia> typeintersect(Tuple, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{Any}}, Tuple{Int,Float64})
Tuple{Int64, Float64}

但是,如果具有可变数量参数(Vararg)的元组类型具有自由变量,则可以描述不同类型的元组。:

julia> typeintersect(Tuple{Vararg{T} where T}, Tuple{Int,Float64})
Tuple{Int64, Float64}

julia> typeintersect(Tuple{Vararg{T}} where T, Tuple{Int,Float64})
Union{}

请注意,当类型’T`相对于类型’Tuple’是自由的(即它的绑定类型’unionAll’在类型`Tuple’之外)时,只有一个值`T`应该与整个类型一起工作。 因此,不同元组不匹配。

最后,值得注意的是,`Tuple{}`是不同的。:

julia> Tuple{}
Tuple{}

julia> Tuple{}.parameters
svec()

julia> typeintersect(Tuple{}, Tuple{Int})
Union{}

什么是"主"元组类型?

julia> pointer_from_objref(Tuple)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{})
Ptr{Cvoid} @0x00007f5998a570d0

julia> pointer_from_objref(Tuple.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

julia> pointer_from_objref(Tuple{}.name.wrapper)
Ptr{Cvoid} @0x00007f5998a04370

因此’+元组==元组{Vararg{Any}}’确实是主要类型。

对角线类型

考虑’元组’类型{T,T} 在哪里T'。 具有此签名的方法将如下所示:

f(x::T, y::T) where {T} = ...

根据类型`unionAll’的通常解释,这种类型’T’适用于所有类型,包括`Any`,因此它应该等同于`Tuple'。{Any,Any}`. 然而,这种解释导致了一些实际问题。

首先,T`的值必须在方法定义中可用。 对于像`f(1,1.0)`这样的调用,目前还不清楚`T`的类型应该是什么。 它可以是一个’联盟{Int,Float64},或者也许, '真实`。 直观地说,我们期望声明’x::T’表示’T===typeof(x)'。 为了确保不变量被保留,我们在这个方法中需要`typeof(x)===typeof(y)===T`。 这意味着只应为完全相同类型的参数调用该方法。

事实证明,发送有关两个值是否具有相同类型的数据的能力非常有用(例如,在推广系统中使用它),因此我们有几个原因可以获得对’元组的不同解释{T,T} 在哪里T'。 为此,我们将以下规则添加到子类型中:如果变量在协变位置出现多次,则仅限于特定类型的范围。 ("协变位置"意味着只有类型`Tuple`和`Union`发生在变量的出现与其输入的类型`unionAll`之间。)这样的变量被称为"对角变量"或"特定变量"。

例如,'元组{T,T} 其中T’可以被视为'Union{Tuple{Int8,Int8},元组{Int16,Int16}...},其中类型’T’涵盖所有特定类型。 这导致了一些有趣的子类型结果。 例如,'元组{Real,Real}`不是’Tuple’类型的子类型{T,T} 其中T',因为它包括一些类型,如’Tuple{Int8,Int16},其中这两个元素具有不同的类型。 '元组{Real,Real}'和’元组{T,T} 其中T’有一个非平凡的交集’元组{T,T} 其中T<:Real'。 但是,'元组{Real}'_是类型'Tuple的子类型{T} 其中T`,因为在这种情况下,类型’T’只发生一次,因此不是对角线。

接下来,考虑以下类型的签名:

f(a::Array{T}, x::T, y::T) where {T} = ...

在这种情况下,类型’T’出现在`数组'内的不变位置。{T}. 这意味着无论传递什么类型的数组,它都唯一地定义了类型`T`的值-我们说类型`T`具有相等约束。 因此,对角线规则在这种情况下是不必要的,因为数组定义了类型`T,并且您可以允许`x`和`y`是类型`T’的任何子类型。 因此,发生在不变位置的变量永远不会被视为对角线。 这种行为选择有些争议-有些人认为这个定义应该写成

f(a::Array{T}, x::S, y::S) where {T, S<:T} = ...

为了澄清`x’和’y’是否应该具有相同的类型。 在此版本的签名中,它们将具有相同的类型,或者如果`x`和`y`可能具有不同的类型,则可以为类型`y`输入第三个变量。

下一个困难是联合和对角线变量的相互作用,例如:

f(x::Union{Nothing,T}, y::T) where {T} = ...

让我们来看看这个广告是什么意思。 'y’是`T’类型。 然后’x’可以有相同的类型’T`,或类型 '没什么`。 因此,以下所有调用都必须匹配:

f(1, 1)
f("", "")
f(2.0, 2.0)
f(nothing, 1)
f(nothing, "")
f(nothing, 2.0)

这些示例显示以下内容:当`x`具有值`nothing’时。::没什么`,对`y`没有额外的限制。 就好像方法签名有`y::Any’一样。 实际上,我们有以下类型等价:

(Tuple{Union{Nothing,T},T} where T) == Union{Tuple{Nothing,Any}, Tuple{T,T} where T}

一般规则是,如果子类型算法只使用一次,则协变位置中的特定变量表现为非特定变量。 当’x`是’Nothing’类型时,您不需要在`Union`中使用`T’类型。{Nothing,T}`,它仅用于第二插槽。 这是由于在'Tuple{T} 其中T`将’T’的类型限制为特定类型并不重要。 类型等于'Tuple{Any}'无论如何。

然而,一旦出现在一个不变的位置,变量就失去了具体的权利,不管是否使用这种情况。 否则,类型的行为可能会有所不同,具体取决于它们与哪些其他类型进行比较,从而使子类型不可传递。 例如,考虑以下内容。

Tuple{Int,Int8,Vector{Integer}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

如果`Union`内的类型`T’被忽略,那么类型`T`将是特定的,答案将是false,因为前两种类型是不一样的。 现在考虑以下内容。

Tuple{Int,Int8,Vector{Any}} <: Tuple{T,T,Vector{Union{Integer,T}}} where T

现在我们不能忽略’Union’中的类型’T`(我们应该有`T==Any`),所以类型`T’不是特定的,答案是真的。 在这种情况下,`T`的具体性将取决于另一种类型,这是不可接受的,因为该类型必须自身具有明确的含义。 因此,在这两种情况下都考虑了"向量"内的"T"的出现。

对角变量的子类型

对角变量的子类型算法由两个部分组成:(1)确定变量的出现和(2)确保对角变量只复盖特定类型。

第一个任务是通过保持计数器`发生_inv`和`发生_cov`(在文件`src/子类型中)来解决的。c`)对于环境中的每个变量,分别跟踪不变和协变出现的次数。 变量在`occens_inv==0&&occens_cov>1`时为对角线。

第二个问题是通过对变量的下界施加条件来解决的。 随着子类型算法的工作,每个变量的边界被缩小(增加下限和减少上限),以便跟踪将执行子类型比率的变量值的范围。 完成了`unionAll’类型主体的计算,其变量是对角线,让我们来看看边界的最终值。 由于变量必须是特定的,因此如果其下界不能是特定类型的子类型,则会产生矛盾。 例如,像这样的抽象类型 'AbstractArray'不能是特定类型的子类型,但像`Int’这样的特定类型可以,空类型`Bottom`也可以。 如果下界未通过此测试,则算法以答案"false"停止。

例如,在`Tuple’任务中{Int,String} <:元组{T,T} 如果T是联合的超类型,那么这个比率是正确的{Int,String}`. 然而,'联盟{Int,String}'是抽象类型,所以关系不成立。

使用’is_leaf_bound`函数执行此特异性测试。 请注意,此测试与`jl_is_leaf_type`函数略有不同,因为它也为下界(Bottom)返回`true`。 目前,此函数是启发式的,不会拦截所有可能的特定类型。 困难在于下界的具体性可能取决于其他类型变量边界的值。 例如,'Vector{T}'相当于特定类型'Vector{Int}`,只有当`T`的上限和下限等于`Int’时。 我们还没有开发出处理这种情况的完整算法。

了解内部机制

大多数处理类型的操作都可以在文件的jltypes中找到。c’和’子类型。c'。 最好从探索行动中的打字开始。 使用`make debug’构建Julia并在调试器中运行Julia。 章中 gdb调试提示你会发现有用的提示。

由于子类型代码在REPL本身中被主动使用,因此此代码中的断点会频繁触发,因此如果创建以下定义会更容易:

julia> function mysubtype(a,b)
           ccall(:jl_breakpoint, Cvoid, (Any,), nothing)
           a <: b
       end

然后在`jl_breakpoint`函数中设置断点。 触发此点后,可以在其他函数中设置断点。

作为热身,请尝试以下操作。

mysubtype(Tuple{Int, Float64}, Tuple{Integer, Real})

让我们提高水平,并考虑一个更复杂的情况。:

mysubtype(Tuple{Array{Int,2}, Int8}, Tuple{Array{T}, T} where T)

子类型和排序方法

'Type_morespecific’函数用于为方法表中的函数设置部分顺序(从最具体到最少)。 特异性是严格的;如果`a`比`b`更具特异性,那么`a`不等于`b`并且`b`不比’a’更具特异性。

如果`a’是`b’的严格子类型,那么它会自动被认为更具体。 基于此,'type_morespecific’使用了一些不太正式的规则。 例如,'subtype’取决于参数的数量,而`type_morespecific’可能不会。 特别是,'元组{Int,AbstractFloat}'比'元组更具体{Integer},即使它不是子类型。 (既不’元组{Int,AbstractFloat},也不是’元组{Integer,Float64}'并不比另一个更具体。)特别是,'+元组{Int,Vararg{Int}}+不是'+元组的子类型{Integer}+,但被认为更具体。 但是,'morespecific’获得长度奖励:具体来说’Tuple{Int,Int}'比`元组更具体{Int,Vararg{Int}}`.

如果您正在调试方法排序的方法,那么最好定义一个函数。:

type_morespecific(a, b) = ccall(:jl_type_morespecific, Cint, (Any,Any), a, b)

它允许您检查元组类型’a`是否比元组类型’b’更具体。