有关类型的其他信息
类型和集合(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’很少需要手动创建(而且,应该避免)。
自由变量
任何类型的变量的概念在类型系统中是非常重要的。 我们说变量’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’更具体。