AnyMath 文档

更多关于类型

如果你已经使用Julia一段时间了,你就会明白类型扮演的基本角色。 在这里,我们试图在引擎盖下,特别关注 参数类型

类型和集(和 任何工会{}/Bottom) /Bottom)}

从集合的角度来设想Julia的类型系统也许是最容易的。 当程序操作单个值时,类型指的是一组值。 这与集合不同;例如 xref:base/collections.adoc#Base.Set[套装 价值观本身就是一个单一的 套装 价值。 相反,类型描述了一组_possible_值,表示我们拥有哪个值的不确定性。

A_concrete_类型 T 描述其直接标记的值集,由 类型;类型功能,是 T. _abstract_类型描述一些可能更大的值集。

任何描述可能值的整个宇宙。 整数任何 这包括 Int型, Int8,和其他混凝土类型。 在内部,Julia也大量使用另一种类型,称为 底部,也可以写成 联合{}. 这对应于空集。

Julia的类型支持集合论的标准操作:你可以问是否 T1 是一个"子集"(子类型) T2T1<:T2. 同样,您使用以下两种类型相交 打字机,打字机,把他们的工会与 工会,并计算一个包含它们与 [医打字]:

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也实现了类型的部分顺序;这是通过类似于 <:,但具有将在下面讨论的差异。

联合所有类型

Julia的类型系统还可以表示类型的_iterated union_:类型在某个变量的所有值上的联合。 这是描述某些参数值未知的参数类型所必需的。

例如, 阵列有两个参数,如 阵列{Int,2}. 如果我们不知道元素类型,我们可以写 阵列{T,2} 在哪里T,这是 阵列{T,2} 对于所有值 T: 联盟{Array{Int8,2},阵列{Int16,2}, ...}.

这样的类型由一个 联合所有 对象,其中包含一个变量(T 在这个例子中,类型 打字机,打字机),和一个包装类型(阵列{T,2} 例中)。

考虑以下方法:

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

签名-如中所述 函数调用-of f3 是一个 联合所有 类型包装元组类型: 元组{typeof(f3), Array{T}}其中T. 除了 f4 可以用 a=[1,2];所有但 f2 可以用 b=任意[1,2].

让我们更仔细地看看这些类型:

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

这表明 阵列 实际上命名a 联合所有 类型。 有一个 联合所有 嵌套的每个参数的类型。 语法 阵列{Int,2} 相当于 数组{Int}{2};内部各 联合所有 用一个特定的变量值实例化,一次一个,最外层-第一。 这给尾随类型参数的省略带来了自然的含义; 数组{Int} 给出一个等效于 阵列{Int,N} 其中N.

A 打字机,打字机 本身不是一个类型,而是应该被认为是一个结构的一部分 联合所有 类型。 类型变量的值有下限和上限(在字段中 磅/磅ub). 符号 姓名 纯粹是化妆品。 内部, 打字机,打字机s按地址进行比较,因此将它们定义为可变类型,以确保可以区分"不同"类型的变量。 但是,按照惯例,它们不应该发生变异。

人们可以建造 打字机,打字机s手动:

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

有一些方便的版本允许您省略这些参数中的任何一个,除了 姓名 符号。

语法 数组{T} 其中T<:整数 被降低到

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

所以很少需要构造一个 打字机,打字机 手动(实际上,这是要避免的)。

自由变量

_free_类型变量的概念在类型系统中非常重要。 我们说一个变量 V 是免费的类型 T 如果 T 不包含 联合所有 这引入了变量 V. 例如,类型 数组{Array{V} 其中V<:整数} 没有自由变量,但 数组{V} 它内部的一部分确实有一个自由变量, V.

从某种意义上说,具有自由变量的类型根本不是真正的类型。 考虑类型 数组{Array{T}}其中T,其是指阵列的所有同质阵列。 内部类型 数组{T},看到自己,似乎是指任何类型的数组。 但是,外部数组的每个元素都必须具有_same_数组类型,因此 数组{T} 不能只引用任何旧数组。 可以这么说 数组{T} 有效地"发生"多次,并且 T 每个"时间"必须具有相同的值。

为此,功能 jl_has_free_typevars 在C API中是非常重要的。 它返回true的类型不会在子类型和其他类型函数中给出有意义的答案。

打字名称

以下两个 阵列类型在功能上是等效的,但打印方式不同:

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

julia> Array
Array

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

这些可以通过检查区分 姓名 类型的字段,它是类型的对象 类型名称:

julia> dump(Array{Int,1}.name)
TypeName
  name: Symbol Array
  module: Module Core
  singletonname: Symbol Array
  names: SimpleVector
    1: Symbol ref
    2: Symbol size
  atomicfields: Ptr{Nothing}(0x0000000000000000)
  constfields: Ptr{Nothing}(0x0000000000000000)
  wrapper: UnionAll
    var: TypeVar
      name: Symbol T
      lb: Union{}
      ub: abstract type Any
    body: UnionAll
      var: TypeVar
        name: Symbol N
        lb: Union{}
        ub: abstract type Any
      body: mutable struct Array{T, N} <: DenseArray{T, N}
  Typeofwrapper: abstract type Type{Array} <: Any
  cache: SimpleVector
    ...
  linearcache: SimpleVector
    ...
  hash: Int64 2594190783455944385
  backedges: #undef
  partial: #undef
  max_args: Int32 0
  n_uninitialized: Int32 0
  flags: UInt8 0x02
  cache_entry_count: UInt8 0x00
  max_methods: UInt8 0x00
  constprop_heuristic: UInt8 0x00

在这种情况下,相关字段是 包装器,包装器,其中包含对用于创建new的顶级类型的引用 阵列 类型。

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

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

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

julia>pointer_from_objref(数组{TV,NV}.name.包装器)
Ptr{Cvoid} @0x00007fcc7de64850

包装器,包装器 的领域 阵列指向自己,但为 阵列{TV,NV} 它指向类型的原始定义。

其他领域呢? 哈希 为每种类型分配一个整数。 检查 缓存 字段,选择一个比数组使用较少的类型是有帮助的。 我们先创建自己的类型:

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::元组,类型必须能够容纳任何元组。 让我们检查参数:

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}

但是,如果一个变量(瓦拉格)元组类型有自由变量它可以描述不同种类的元组:

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 是自由相对于 元组 型(即其绑定 联合所有 类型在 元组 型),只有一个 T 值必须适用于整个类型。 因此异构元组不匹配。

最后,值得注意的是 元组{} 是不同的:

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} = ...

根据a的通常解释 联合所有 型,这个 T 所有类型的范围,包括 任何,所以这种类型应该等同于 元组{Any,Any}. 然而,这种解释引起了一些实际问题。

首先,一个值 T 需要在方法定义中可用。 像这样的电话 f(1,1.0),目前尚不清楚是什么 T 应该是。 可能是 工会{Int,Float64},或者也许 真实的. 直觉上,我们期望宣言 x::T 意思是 T===类型(x). 为了确保不变性成立,我们需要 typeof(x)===typeof(y)===T 在这种方法中。 这意味着该方法应该只为完全相同类型的参数调用。

事实证明,能够调度两个值是否具有相同的类型是非常有用的(例如,这是由提升系统使用的),因此我们有多种理由想要对 元组{T,T} 在哪里T. 为了使这项工作,我们在子类型中添加了以下规则:如果一个变量在协变位置出现多次,则它仅限于具体类型的范围。 ("协变位置"意味着仅 元组工会 类型发生在变量的出现和 联合所有 引入它的类型。)这样的变量被称为"对角变量"或"具体变量"。

例如, 元组{T,T} 在哪里T 可以看作是 联盟{Tuple{Int8,Int8},元组{Int16,Int16}, ...},在哪里 T 适用于所有混凝土类型。 这产生了一些有趣的子类型结果。 例如 元组{Real,Real} 不是 元组{T,T} 在哪里T,因为它包括一些类型,如 元组{Int8,Int16} 其中两个元素具有不同的类型。 元组{Real,Real}元组{T,T} 在哪里T 有不平凡的交集 元组{T,T} 其中T<:Real. 然而, 元组{Real} _is_的子类型 元组{T} 其中T,因为那样的话 T 只发生一次,所以不是对角线。

接下来考虑如下所示的签名:

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

在这种情况下, T 发生在不变的位置内 数组{T}. 这意味着传递的任何类型的数组都明确地确定 T --我们说 T 它有一个相等的约束。 因此,在这种情况下,对角线规则并不是真正必要的,因为数组决定了 T 然后我们可以允许 xy 是任何亚型的 T. 因此,发生在不变位置的变量永远不会被视为对角线。 这种行为选择略有争议-有些人觉得这个定义应该写成

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

澄清是否 xy 需要具有相同的类型。 在这个版本的签名中,他们会,或者我们可以为 y 如果 xy 可以有不同的类型。

下一个复杂的问题是联合和对角线变量的相互作用,例如

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没什么::没什么,没有额外的限制 y. 就好像方法签名有 y::任何. 实际上,我们有以下类型等价:

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

一般规则是:协变位置的具体变量的行为就像它不是具体的,如果子类型算法只_uses_它一次。 何时 x 有类型 什么都没有,我们不需要使用 T工会{Nothing,T};我们只在第二个插槽中使用它。 这是自然产生的观察,在 元组{T} 其中T 限制 T 对混凝土类型没有区别;类型等于 元组{Any} 不管怎样。

但是,无论是否使用变量的外观,出现在_invariant_位置都会使变量失去具体的资格。 否则,类型的行为可能会有所不同,具体取决于它们与哪些其他类型进行比较,从而使子类型不会传递。 例如,考虑

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

如果 T工会 被忽略,那么 T 是具体的,答案是"假",因为前两种类型是不一样的。 但要考虑一下

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

现在我们不能忽视 T工会 (我们必须有 T==任何),所以 T 不是具体的,答案是"真实的"。 这将使 T 依赖于另一种类型,这是不可接受的,因为类型必须自己具有明确的含义。 因此, T 里面 向量资料 在这两种情况下都被考虑。

对角变量的子类型

对角变量的子类型算法有两个组成部分:(1)识别变量出现,(2)确保对角变量仅在具体类型范围内。

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

第二个任务是通过对变量的下限施加条件来完成的。 当子类型算法运行时,它会缩小每个变量的边界(提高下限和降低上限),以跟踪子类型关系将保留的变量值范围。 当我们完成评估的身体 联合所有 变量为对角线的类型,我们查看边界的最终值。 由于变量必须是具体的,因此如果其下限不能是具体类型的子类型,则会发生矛盾。 例如,像这样的抽象类型 抽象阵列不能是具体类型的子类型,而是具体类型,如 Int型 可以,而空型 底部 可以为好。 如果下界未通过此测试,则算法将以答案停止 错误.

例如,在问题 元组{Int,String} <:元组{T,T} 在哪里T,我们得出这将是真实的,如果 T 是一个超类型的 工会{Int,String}. 然而, 工会{Int,String} 是抽象类型,因此关系不成立。

这个具体的测试是由函数完成的 is_leaf_bound. 请注意,此测试与 jl_is_leaf_type,因为它也返回 真的底部. 目前这个函数是启发式的,并且没有捕获所有可能的具体类型。 困难在于下界是否具体可能取决于其他类型变量bounds的值。 例如, 向量{T} 相当于混凝土类型 向量{Int} 只有当上下界 T 平等 Int型. 我们还没有制定出一个完整的算法。

内部机械简介

大多数处理类型的操作都可以在文件中找到 jltypes。c子类型。c. 一个好的开始方法是观察子类型的操作。 建立朱莉娅与 进行调试 并在调试器中启动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)

子类型和方法排序

类型_专业 函数用于对方法表中的函数施加部分顺序(从最大到最小特定)。 特异性是严格的;如果 a 比…​…​更具体。 b,则 a 不等于 bb 不是比 a.

如果 a 是一个严格的子类型 b,则自动认为更具体。 从那里开始, 类型_专业 采用一些不太正式的规则。 例如, 子类型 对参数的数量很敏感,但是 类型_专业 可能不是。 特别是, 元组{Int,AbstractFloat} 比…​…​更具体。 元组{Integer},即使它不是子类型。 (的 元组{Int,AbstractFloat}元组{Integer,Float64},两者都没有比另一个更具体。)同样, 元组{Int,Vararg{Int}} 不是 元组{Integer},但它被认为更具体。 然而, 更具体 确实获得了长度的奖励:特别是, 元组{Int,Int} 比…​…​更具体。 元组{Int,Vararg{Int}}.

此外,如果每个类型都定义了两个具有相同签名的方法-相等,那么它们将通过添加顺序进行比较,这样后面的方法比前面的方法更具体。