Julia(转换和推广)

2021-04-14 13:38:31 浏览数 (3)

转换和推广

Julia有一个用于将数学运算符的参数提升为通用类型的系统,在其他各个部分中都提到了该系统,包括整数和浮点数,数学运算和基本函数,类型和方法。在本节中,我们将说明此提升系统如何工作,以及如何将其扩展为新类型并将其应用于除内置数学运算符之外的函数。传统上,就促进算术参数而言,编程语言分为两个阵营:

  • 自动提升内置算术类型和运算符。在大多数语言中,内置数字类型,操作数的算术运算符与缀语法,如使用时 -*,和/,会自动提升为普通型,以产生预期的效果。C,Java,Perl和Python等都可以正确地将和计算1 1.5为浮点值2.5,即使其中一个操作数 是一个整数。这些系统很方便且设计得足够仔细,以至于程序员通常几乎看不见它们:几乎没有人在编写这样的表达式时有意识地想到这种提升,但是编译器和解释器必须在加法之前执行转换,因为整数和浮点数-点值不能按原样添加。因此,此类自动转换的复杂规则不可避免地是此类语言的规范和实现的一部分。
  • 没有自动升级。该阵营包括Ada和ML –非常“严格”的静态类型语言。在这些语言中,每次转换都必须由程序员明确指定。因此,示例表达式1 1.5在Ada和ML中都是编译错误。相反,必须执行write real(1) 1.51然后在执行加法之前将整数显式转换为浮点值。到处都是显式转换非常不方便,但是,即使Ada也具有一定程度的自动转换:整数文字会自动提升为所需的整数类型,而浮点文字也同样会提升为适当的浮点类型。

从某种意义上说,Julia属于“没有自动提升”类别:数学运算符只是具有特殊语法的函数,而函数的参数永远不会自动转换。但是,可能会观察到,将数学运算应用于多种混合参数类型只是多态多重分派的极端情况-朱莉娅的分派和类型系统特别适合处理这种情况。数学操作数的“自动”升级只是作为一种特殊应用出现:Julia附带了针对数学运算符的预定义全部捕获调度规则,当对操作数类型的某种组合不存在特定实现时调用。这些通用规则首先使用用户可定义的提升规则将所有操作数提升为一个通用类型,然后为产生的值(现在是相同类型)调用有问题的运算符的专用实现。用户定义的类型可以通过定义用于与其他类型之间进行转换的方法,并提供一些促销规则来定义它们与其他类型混合时应提升为哪种类型,从而轻松地参与此促销系统。

转换次数

值到各种类型的转换由convert功能执行。该convert函数通常带有两个参数:第一个是类型对象,第二个是要转换为该类型的值;第二个是要转换为该类型的值。返回的值是转换为给定类型的实例的值。理解此功能的最简单方法是查看其功能:

代码语言:javascript复制
julia> x = 12
12

julia> typeof(x)
Int64

julia> convert(UInt8, x)
0x0c

julia> typeof(ans)
UInt8

julia> convert(AbstractFloat, x)
12.0

julia> typeof(ans)
Float64

julia> a = Any[1 2 3; 4 5 6]
2×3 Array{Any,2}:
 1  2  3
 4  5  6

julia> convert(Array{Float64}, a)
2×3 Array{Float64,2}:
 1.0  2.0  3.0
 4.0  5.0  6.0

转换并非总是可能的,在这种情况下,将引发任何方法错误,指示convert不知道如何执行请求的转换:

代码语言:javascript复制
julia> convert(AbstractFloat, "foo")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
This may have arisen from a call to the constructor AbstractFloat(...),
since type constructors fall back to convert methods.

某些语言认为将字符串解析为数字或将数字格式解析为要转换的字符串(许多动态语言甚至会自动为您执行转换),但是Julia却没有:即使某些字符串可以解析为数字,但大多数字符串都不是数字的有效表示形式数字,并且只有非常有限的一部分。因此,在Julia中parse()必须使用专用功能来执行此操作,从而使其更加明确。

定义新的转化

要定义新的转化,只需为提供新方法convert()。这就是全部。例如,将实数转换为布尔值的方法是这样的:

代码语言:javascript复制
convert(::Type{Bool}, x::Real) = x==0 ? false : x==1 ? true : throw(InexactError())

类型该方法的第一个参数是一个单型,Type{Bool},唯一的实例,它的是Bool。因此,仅当第一个参数是类型value时才调用此方法Bool。注意第一个参数的语法:在::符号前省略参数名称,只给出类型。这是Julia在Julian中指定函数类型但其值从未在函数主体中使用的函数参数的语法。在此示例中,由于类型是单例,因此永远没有理由在主体内使用其值。调用该方法时,该方法通过将数字值与1和0进行比较来确定数字值是布尔值是true还是false:

代码语言:javascript复制
julia> convert(Bool, 1)
true

julia> convert(Bool, 0)
false

julia> convert(Bool, 1im)
ERROR: InexactError()
Stacktrace:
 [1] convert(::Type{Bool}, ::Complex{Int64}) at ./complex.jl:31

julia> convert(Bool, 0im)
false

转换方法的方法签名通常比此示例要复杂得多,尤其是对于参数类型。上面的示例仅用于教学目的,而不是实际的Julia行为。这是Julia中的实际实现:

代码语言:javascript复制
convert(::Type{T}, z::Complex) where {T<:Real} =
    (imag(z) == 0 ? convert(T, real(z)) : throw(InexactError()))

案例研究:理性转换

为了继续进行Julia Rational类型的案例研究,以下rational.jl是在类型及其构造函数的声明之后在中声明的转换:

代码语言:javascript复制
convert(::Type{Rational{T}}, x::Rational) where {T<:Integer} = Rational(convert(T,x.num),convert(T,x.den))
convert(::Type{Rational{T}}, x::Integer) where {T<:Integer} = Rational(convert(T,x), convert(T,1))

function convert(::Type{Rational{T}}, x::AbstractFloat, tol::Real) where T<:Integer
    if isnan(x); return zero(T)//zero(T); end
    if isinf(x); return sign(x)//zero(T); end
    y = x
    a = d = one(T)
    b = c = zero(T)
    while true
        f = convert(T,round(y)); y -= f
        a, b, c, d = f*a c, f*b d, a, b
        if y == 0 || abs(a/b-x) <= tol
            return a//b
        end
        y = 1/y
    end
end
convert(rt::Type{Rational{T}}, x::AbstractFloat) where {T<:Integer} = convert(rt,x,eps(x))

convert(::Type{T}, x::Rational) where {T<:AbstractFloat} = convert(T,x.num)/convert(T,x.den)
convert(::Type{T}, x::Rational) where {T<:Integer} = div(convert(T,x.num),convert(T,x.den))

最初的四种转换方法可将转换为有理类型。第一种方法是通过将分子和分母转换为适当的整数类型,从而将一种有理数转换为另一种有理数。第二种方法通过将分母设为1来对整数进行相同的转换。第三种方法实现了一种标准算法,该算法通过整数与给定公差之内的比率来近似浮点数,第四种方法使用以给定值作为阈值的机器epsilon。通常,应该有一个a//b == convert(Rational{Int64}, a/b)

最后两种转换方法提供了从有理类型到浮点数和整数类型的转换。要转换为浮点数,只需将分子和分母都转换为该浮点类型,然后进行除法。要转换为整数,可以使用div运算符对整数进行截断(四舍五入)。

晋升

升级是指将混合类型的值转换为单个普通类型。尽管不是严格必须的,但是通常暗示将值转换为的通用类型可以忠实地表示所有原始值。从这个意义上说,术语“促销”是适当的,因为这些值被转换为“更大”的类型,即可以代表一个通用类型的所有输入值的类型。但是,重要的是不要将其与面向对象(结构)的超级类型或Julia的抽象超类型的概念相混淆:提升与类型层次结构无关,而与在备用表示形式之间进行转换有关。例如,尽管每个Int32值也可以表示为一个Float64值,Int32Float64

promote函数Julia中,该函数执行提升为通用“更大”类型的操作,该函数采用任意数量的参数,并返回相同数量的值的元组,转换为通用类型,如果无法提升则抛出异常。提升的最常见用例是将数字参数转换为常见类型:

代码语言:javascript复制
julia> promote(1, 2.5)
(1.0, 2.5)

julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)

julia> promote(2, 3//4)
(2//1, 3//4)

julia> promote(1, 2.5, 3, 3//4)
(1.0, 2.5, 3.0, 0.75)

julia> promote(1.5, im)
(1.5   0.0im, 0.0   1.0im)

julia> promote(1   2im, 3//4)
(1//1   2//1*im, 3//4   0//1*im)

浮点值被提升为最大的浮点参数类型。整数值被提升为本地机器字大小或最大整数参数类型中的较大者。整数和浮点值的混合被提升为足以容纳所有值的浮点类型。混合有理的整数被提升为有理。混有浮点数的有理数被提升为浮点数。将复杂值与实际值混合会提升为适当类型的复杂值。

这实际上是使用促销的全部内容。其余的就是巧妙应用程序的问题,最典型的“聪明”的应用是对于像算术运算符数字运算捕获所有方法的定义 -*/。以下是一些通用方法定义promotion.jl

代码语言:javascript复制
 (x::Number, y::Number) =  (promote(x,y)...)
-(x::Number, y::Number) = -(promote(x,y)...)
*(x::Number, y::Number) = *(promote(x,y)...)
/(x::Number, y::Number) = /(promote(x,y)...)

这些方法定义表明,在没有用于对数字值进行加,减,乘和除的更具体规则的情况下,请将这些值提升为通用类型,然后重试。这就是全部内容:无需再担心升级为用于算术运算的通用数字类型了-它会自动发生。在中,还有许多其他算术和数学函数的包罗万象的提升方法的定义promotion.jl,但除此之外,promoteJulia标准库中几乎没有任何要求的调用。最常见的用法promote为了方便起见,在外部构造函数方法中会发生这种情况,以允许具有混合类型的构造函数调用委派给内部类型,并将内部字段提升为适当的公共类型。例如,回想rational.jl提供了以下外部构造方法:

代码语言:javascript复制
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)

这样可以进行如下调用:

代码语言:javascript复制
julia> Rational(Int8(15),Int32(-5))
-3//1

julia> typeof(ans)
Rational{Int32}

对于大多数用户定义的类型,更好的做法是要求程序员为构造函数显式提供期望的类型,但是有时,尤其是对于数字问题,自动进行提升会很方便。

定义促销规则

尽管原则上可以promote直接为该函数定义方法,但这将需要对参数类型的所有可能排列进行许多冗余的定义。取而代之的是,它的行为promote是通过称为的辅助函数定义的,该函数promote_rule可以为其提供方法。该promote_rule函数接受一对类型对象,然后返回另一个类型对象,这样参数类型的实例将被提升为返回的类型。因此,通过定义规则:

代码语言:javascript复制
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64

有人宣称,将64位和32位浮点值一起提升时,应将它们提升为64位浮点。提升类型不必是参数类型之一。以下促销规则均出现在Julia的标准库中:

代码语言:javascript复制
promote_rule(::Type{UInt8}, ::Type{Int8}) = Int
promote_rule(::Type{BigInt}, ::Type{Int8}) = BigInt

在后一种情况下,结果类型是BigInt因为BigInt它是唯一足以容纳用于任意精度整数算术的整数的类型。另请注意,不必同时定义两者,promote_rule(::Type{A}, ::Type{B})并且promote_rule(::Type{B}, ::Type{A})promote_rule促销过程中使用的方式暗含着对称性。

promote_rule函数用作定义第二个函数的构造块,该函数在promote_type给定任意数量的类型对象的情况下,将这些值返回的公共类型作为promote应推广的参数。因此,如果要在没有实际值的情况下知道某些类型的值的集合将提升为哪种类型,可以使用promote_type

代码语言:javascript复制
julia> promote_type(Int8, UInt16)
Int64

在内部,promote_type用于promote确定应将哪种类型的参数值转换为提升值。但是,它本身可以很有用。好奇的读者可以阅读中的代码promotion.jl,该代码在大约35行中定义了完整的升级机制。

案例研究:理性促销

最后,我们完成了正在进行的Julia有理数类型的案例研究,该案例对晋升机制具有以下晋升规则进行了相对复杂的使用:

代码语言:javascript复制
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{Rational{S}}) where {T<:Integer,S<:Integer} = Rational{promote_type(T,S)}
promote_rule(::Type{Rational{T}}, ::Type{S}) where {T<:Integer,S<:AbstractFloat} = promote_type(T,S)

第一条规则说,用任何其他整数类型提升有理数会提升为分子/分母类型是其分子/分母类型也提升了其他整数类型的结果的有理类型。第二条规则将相同的逻辑应用于两种不同类型的有理数,从而导致它们各自的分子/分母类型的有理化。第三条也是最后一条规则规定,使用浮点数推广有理数与使用浮点数推广分子/分母类型的结果相同。

少量的升级规则以及上面讨论的转换方法,足以使有理数与Julia的所有其他数字类型(整数,浮点数和复数)完全自然地互操作。通过以相同的方式提供适当的转换方法和升级规则,任何用户定义的数字类型都可以自然地与Julia的预定义数字进行互操作。

0 人点赞