Julia参数化抽象类型

Julia参数化抽象类型

定义

在Julia中,若要定义一个参数化的抽象类型,基本语法为:

abstract type类型名{T1, T2, ...} end

其中,“类型名”是待定义的抽象类型名称,大括号中的是各种类参列表。

注意
语法中的“类型名”与左大括号之间不能有空格。

仍继续关注坐标点的例子。假设为二维点定义Point2D类型如下:

julia> mutable struct Point2D{T}
          x::T
          y::T
        end

之后,对其定义求向量模的计算:

mymodule(p::Point2D) = sqrt(p.x^2 + p.y^2)      # 定义了名称为mymodule的函数

显然调用时需提供Point2D类型的参数才能正常获得结果,例如:
Julia参数化抽象类型

而采用Point1D等其他类型是不允许的,例如:
Julia参数化抽象类型

在以Point1D类型作为参数调用时会报MethodError错误,并提示未找到匹配的方法。可以想见,对Point3D也同样如此。

当然此函数的参数p也可以限定为Point2D的某个具象类型,例如:

mymodule(p::Point2D{Int64}) = sqrt(p.x^2 + p.y^2)

显然此方法限制性过大,徒增烦恼;而之前以参数化类型的名称限定p的类型更具普适性。

如果我们需要对前文中的Point1D与Point3D也提供同样的求向量模功能函数,显然需为这两个类型提供单独的定义,即再定义两个计算函数。如果应用中存在更多维的坐标类型,同时功能函数不仅仅求模值这一种,那么代码量将骤增,而且是枯燥地重复性增加,会给后续的维护带来麻烦。所以,我们需要一种抽象的坐标类型,能够承载任意维度坐标共有的计算操作。

为此定义一个名为Pointy的抽象类型,并进行参数化,如下:

julia> abstract type Pointy{T} end

如果T被指定,获得的所有具象化类型均为Pointy的子类型,例如:

julia> Pointy{Int64} <: Pointy
true

julia> Pointy{Float64} <: Pointy
true

更为灵活的是,参数T还可以取具体的数值:

julia> Pointy{1} <: Pointy
true

这对于内部成员有元素数量可动态延展的结构(如后面介绍的数组),会非常有用。

对于声明的参数化抽象类型,不同具象类型之间同样无父子关系,例如:

julia> Pointy{Float64} <: Pointy{Real}
false

虽然Real是Float64的父类型,但以它们为基础的具象类型不具有相容性。

应用

实际上,如抽象类型或元类型那样,类型之间的继承关系是需要在声明时显式确定的。对于之前声明过的Point1DPoint2DPoint3D,完全可以在声明时作为抽象坐标类型Pointy的子类型。所以将它们的声明方式修改如下:

julia> mutable struct Point1D{T} <: Pointy{T}
          x::T
        end

julia> mutable struct Point2D{T} <: Pointy{T}
          x::T
          y::T
        end

julia> mutable struct Point3D{T} <: Pointy{T}
          x::T
          y::T
          z::T
        end

此时三种的参数化类型便成为父参数化类型Pointy的子类型,即:

Point1D <: Pointy
Point2D <: Pointy
Point3D <: Pointy

又因为具象类型是参数化类型的子类型,所以子参数化类型的具象类型均是父参数化类型的子类型,即如下的父子关系断言也是成立的:

Point1D{Float64} <: Pointy
Point2D{Integer} <: Pointy

但父子参数化类型的具象化类型之间仍不存在父子关系,例如:

julia> Point2D{Integer} <: Pointy{Real}
false

虽然其中的类参具备父子关系。

如此一来,我们便可以在Pointy上定义一个统一的操作函数,便能适用于三个不同维度的坐标点类型,而不需要更多针对性的重复性函数。重新定义向量求模函数如下:

        function module_t(p::Pointy)   # 函数的定义方式会在后面详述
          m = 0
          for field in fieldnames(p)   # 遍历成员字段名
            m += getfield(p, field)^2   # 取得该成员值并平方
          end
          sqrt(m)                       # 将多成员值平方和开方
        end

该函数能够适应不同数量字段的复合类型。此时便可用其求解不同维度坐标点的模值,例如:

julia> module_t(Point1D{Int64}(1))
1.0

julia> module_t(Point2D{Float64}(2, 2))
2.8284271247461903

julia> module_t(Point3D{Complex}(1+2im, 3+4im, 5-6im))
2.9389894877329246-5.44404805351722im

可见,子类型沿袭父类型的计算规则能够给开发带来巨大的便利。

不过上例因涉及反射(Reflection)机制,未必是最佳实践,仅作为示例使用。

Type{T}

前面提及,参数化类型Type{T}是Union及Core.TypeofBottom的父类型。实际上,该类型同时也是DataType的父类型,即:

julia> supertype(DataType)
Type{T}

julia> supertype(Type)
Any

Type{T}的父类型才是Any类型。

在Julia内部,Type{T}是一种非常特殊的参数化抽象类型,也是单例类型的一种,可以认为是类型的生成器。从定义层面讲,每个具象的T,都是Type{T}类型的唯一实例对象。为便于理解,先看一些例子:

julia> isa(Float64, Type{Float64})
true

julia> isa(Real, Type{Real})
true

julia> isa(Point2D, Type{Point2D})
true

julia> isa(Pointy, Type{Pointy})
true

但对于不一致的T,这种判断不会成立。例如:

julia> isa(Float64, Type{Real})
false

julia> isa(Point2D, Type{Pointy})
false

而且具体的值也不是Type的实例:

julia> isa(1, Type)
false

julia> isa("foo", Type)
false

可以说,若isa(A, Type{B})(A是Type{B}的实例)成立,有且仅有一种情况:A与B完全相同。

如果结构中没有参数T, Type便是一个简单的参数抽象类型;此时,所有的类型都将是它的实例对象,也包括Type本身,即:

julia> isa(Type{Float64}, Type)
true

julia> isa(Float64, Type)
true

julia> isa(Real, Type)
true

julia> isa(DataType, Type)
true

julia> isa(Type, Type)
true

在酷客教程下文说明参数化原理时,我们还会介绍另一种Type{T}单例类型UnionAll。

酷客网相关文章:

赞(0)

评论 抢沙发

评论前必须登录!