Julia拥有提升数学操作参数为普通类型的系统,在各种别的文中曾被提到,包括【整型数字和浮点型数字】、【算术操作符和基本函数】、【类型】和【方法】。 本文中,解释提升系统如何工作,以及如何扩展之、应用到新的类型,不止内建的数学操作。 传统中,编程语言根据算术参数提升分为两大阵营:
-
内建算术类型和操作自动提升
大多数编程语言中,内建数值类型,以中缀语法用于算术操作的被操作数时,如
+
、-
、*
、/
等,自动提升参与操作的数值为共同类型产出期望的结果。C/Java/Perl/Python,举几个例子来说(to name a few),1+1.5
求和的全部正确计算即浮点值2.5
,尽管被操作数之一是整数。这类系统便捷且被细心设计得足以让程序员通常无感(超薄劲爽):在编写这种些表达式得时候几乎没有程序员有意识地思考提升地发生,但编译器和解释器必须在做加法之前执行转换,因为整数和浮点数不能囫囵(as-is)相加。此种转换的复杂规则是这些编程语言规格和实现不可避免的部分。 -
无自动提升
这一帮包括Ada/ML(变态严格的静态类型编程语言)。这些编程语言中,每个转换都必须由程序员明确指定。因此,例如表达式
1+1.5
会是编译错误(Ada/ML)。相反,必须写为real(1)+1.5
。到处显式转换如此不方便,然而,Ada有某种程度的自动转换:整数字面值自动提升为期望的整数类型,浮点字面值近似提升为恰当的浮点数值类型。
某种意义上(in a sense),Julia落入(fall into)“无自动提升”范畴:算术操作只是拥有特殊语法的函数,函数的参数绝不会被自动转换。 然而,可以观察到,应用算术操作到各种混合参数类型只是多态多重载的极端情况——Julia的重载和类型系统特别适合处理的某些东西。 算术操作的“自动”提升简单暴露为特殊应用:Julia与生俱来的预定义捕捉算术操作的全部重载规则,当不存在某些被操作数类型组合的匹配实现时被调用。 这些捕捉全部规则首先以用户可定义的提升规则提升所有被操作数为共同类型,然后调用结果值成问题的指定实现,这时候被操作数类型是一致的。 用户自定义类型可简单参与改提升系统,定义转换方法(转成别的类型或从别的类型转化)即可,提供一小撮(a handful of)提升规则,定义当混合别的类型的的类型提升为什么类型。
获取某一类型T
的值的标准方法是调用该类型的构造方法,T(o)
。
然而,存在便于将值从一个类习转换为另一个类型、而无须程序员显式要求的情况。
一个例子就是给数组赋值:如果A
是Vector{Float64}
,表达式A[1]=2
应当自动转换2
,从Int
到Float64
,并将结果存入数组。
这可通过convert
函数完成。
这个convert
函数通常带两个参数:前一个是类型对象,后一个是要被转为前述类型的值。
返回值是转换为给定类型的实例。
理解该函数最简单的方式是看实践:
julia> huaan = 9527
9527
julia> typeof(huaan)
Int64
julia> convert(UInt16, huaan)
0x2537
julia> typeof(ans)
UInt16
julia> convert(AbstractFloat, huaan)
9527.0
julia> whoredom = Any[0 1 2; 250 9527 1314]
2×3 Array{Any,2}:
0 1 2
250 9527 1314
julia> convert(Array{Float64}, whoredom)
2×3 Array{Float64,2}:
0.0 1.0 2.0
250.0 9527.0 1314.0
不总是可能被成功转换,这些失败的情况抛出MethodError
表明convert
函数不知道如何执行请求的转换:
julia> convert(AbstractFloat, "cto")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
Closest candidates are:
convert(::Type{T<:Number}, ::T<:Number) where T<:Number at number.jl:6
convert(::Type{T<:Number}, ::Number) where T<:Number at number.jl:7
convert(::Type{T<:Number}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250
...
Stacktrace:
[1] top-level scope at none:0
# 注意
julia> convert(AbstractFloat, "0.0")
ERROR: MethodError: Cannot `convert` an object of type String to an object of type AbstractFloat
Closest candidates are:
convert(::Type{T<:Number}, ::T<:Number) where T<:Number at number.jl:6
convert(::Type{T<:Number}, ::Number) where T<:Number at number.jl:7
convert(::Type{T<:Number}, ::Base.TwicePrecision) where T<:Number at twiceprecision.jl:250
...
Stacktrace:
[1] top-level scope at none:0
某些编程语言考虑解析字符串为数字或格式化数字为字符串纳入转换(很多动态编程语言甚至自动执行转换),然而,Julia不会这样做:即使某些字符串可被解析为数值,大多字符串并非有效的数字(字面),仅非常有限的字符串是。
因此Julia中专用的parse
函数必须用来自行该操作,使其更光明磊落。
下述编程语言概念调用convert
函数:
- 给数组赋值,将值转换为数组元素类型;
- 给对象字段赋值,将值转换为字段声明的类型;
- 用
new
构造对象,将字段值转换为声明的字段类型; - 给声明类型(如
local o::T
)赋值,将值转换为该声明的类型; - 带声明返回值类型的函数转换返回值为该声明的类型;
- 传递值给
ccall
转换值为对应的参数类型。
注意convert(T, o)
的行为貌似(appear to be)差不多和T(o)
一样。
的确,通常确实如此。
然而,有关键的语义不同:因为convert
可被隐式调用,它的方法被限定在认为是安全或不出意外的情况。convert
仅在表达某些相同基本事务的类型(例如各种数字表达或不同字符编码的字符串)。
通常convert
也是无损的;转换一个值到不同类型且转回原型,应该得到精确一致的值。
有四个构造方法明显有别于convert
的情况:
-
与其参数无关的类型构造方法
某些构造方法并未实现转换的概念。例如
Timer(1314)
创建了一个“1314秒”的定时器,这的确不是把一个整数转换为一个定时器。 -
可变集合
convert(T, o)
期望返回原始的o
,如果o
已经是T
类型。 相反,如果T
是可变集合类型,则T(o)
应当总是创造新的集合(从o
的元素拷贝)。 -
包装类型
对于某些包装别的值的类型,构造方法应当在新的对象内包装其参数,即使是已经是所要求的类型。 例如
Some(o)
包装o
表明呈现一个值(上下文中结果应当是Some
或nothing
)。 然而,o
本身应当是Some(oo)
对象,这种情况中,结果是Some(Some(oo))
,有两层包装。convert(Some, o)
,另一方面,应当只返回o
,因其已经是Some
。 -
构造方法不返回自身类型的实例
在非常稀有的情况,对于返回不是
T
类型对象的构造方法T(o)
是有意义的。 如果包装类型是其本身的逆(如Flip(Flip(o)) === o
)或者因库重构为了向后兼容而支持旧式调用语法,这会发生。 但convert(T, o)
应当总是返回类型T
的值。 -
定义新的转换
当定义新的类型时,起初地所有创建方法应当定义为构造方法。 如果变得清晰,隐式转换会有用,有些构造方法匹配上述安全标准(criteria),则
convert
方法可以被添加。 这些方法令人发指地(典型地)非常简单,因为仅须要调用合适的构造方法。 这些定义可能看起来如是:convert(::Type{MyType}, x) = MyType(x)
该方法的第一个参数的类型是【单例类型】,
Type{MyType}
,是MyType
唯一的实例。 因此,第一个参数是MyType
类型时,仅调用该方法。 注意,第一个参数所用的语法:::
符号之前的参数名被省略,只给出了类型。 这是Julia中指定了类型但不需要参数名来引用值得函数参数之语法。 在这个例子中,由于该类型是单例,大家已然知道其值并未引用某个参数名。某些抽象类型的所有实例默认为“雷同(sufficiently similar)”,
Julia.Base
提供一个通用的convert
定义。例如,该定义声明通过调用带一个参数的构造方法来convert
任何Number
类型为别的类型是有效的:convert(::Type{T}, x::Number) where {T<:Number>} = T(x)
这意味着新
Number
类型仅须要定义构造方法,因为这个定义用convert
处理。 还提供了一个处理参数已然是要求类型情况的指定转换:convert(::Type{T}, x::T) where {T<:Number>} = x
AbstractString
、AbstractArray
、AbstractDict
也有类似定义。
提升涉及转换混合类型值为单一共同类型。
尽管并非严格必须,通常暗指值要转换为该共同类型能忠实地表达所有原始值。
在这种观念下,术语“提升”是合适的,因为值转换为更宽泛的(greater)类型,例如,可以用单个共同类型表达所有输入值。
这非常重要,然而,不要将此与面向对象(结构的)的超类混淆,或Julia抽象超类概念:提升与类型层级无关,只是候选表达之间的转换。
对实例而言,尽管每个Int32
值可表达为Float64
值,而Int32
不是Float64
的子类型。
提升到共同宽泛类型,Julia通过promote
函数,可带任意个参数,返回相同个数值得元组,转换为相同类型,或抛出异常(若提升不可行)。
提升最常用得情况是转换数值参数为共同类型:
julia> promote(1, 2.5)
(1.0, 2.5)
julia> promote(1, 2.5, 2)
(1.0, 2.5, 2.0)
julia> promote(2, 3//4)
(2//1, 3//4)
julia> promote(1, 2.5, 2, 3//4)
(1.0, 2.5, 2.0, 0.75)
julia> promote(1.5, im)
(1.5 + 0.0im, 0.0 + 1.0im)
julia> promote(3 + 4im, 3//4)
(3//1 + 4//1*im, 3//4 + 0//1*im)
浮点值将提升为最宽泛的浮点参数类型。
整数值提升为本地机器字长或最大整数参数类型那么宽泛的类型(Int
)。
浮点值和整数值的混合值提升为足够大的浮点类型以把持所有值。
整数值混合分数提升为有理数。
有理数混合浮点值提升为浮点值。
复数值混合实数值提升为某种合适的复数值。
提升的适用,上述即是全部。
剩余的只是聪明的应用相关的,最令人发指聪明的应用是给数值操作(如算术操作+
/-
/*
//
等)定义捕捉所有方法。
在promotion.jl
中给出某些捕捉所有方法的定义:
+(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
中定义了数值算术操作和数学函数捕捉所有提升方法,但除此之外,在Julia.Base
几乎没有别的要求调用promote
的。
promote
最常用的是在【外部构造方法】中,简便起见而提供,允许构造方法调用混合类型,委派字段内部类型提升为合适的共同类型。
例如,回想rational.jl
提供下面外部构造方法:
Rational(n::Integer, d::Integer) = Rational(promote(n,d)...)
这允许下面调用有效:
julia> Rational(Int16(1314), Int32(-9527))
-1314//9527
julia> typeof(ans)
Rational{Int32}
对于大多数用户自定义类型,要求程序员显式提供期望的类型是好的实践,但有些时候,特别是数值问题,可便捷地自动提升。
尽管可以,但原则上,直接定义promote
函数的方法,这需要许多冗余定义,为了所有可能的参数类型组合。
相反,promote
的行为被定义在名为promote_rule
辅助函数中,可为这个辅助函数提供方法。
promote_rule
函数有一对类型对象参数、返回别的类型对象,因此参数类型将被提升为返回类型。
因此,定义规则:
promote_rule(::Type{Float64}, ::Type{Float32}) = Float64
声明当Float64
和Float32
一起被提升,则应当提升为Float64
。
然而,被提升到的类型不必是参数类型之一。
下面提升规则发生在Julia.Base
中:
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
:
julia> promote_type(Int16, Int64)
Int64
内部地,promote_type
用在promote
内部,检测参数值要被提升转换到的的类型。
它可以,退一万步,在其正确用途中有用。
好奇的读者可查看promotion.jl
代码,用35行代码定义完整提升机制。
最后,结束Julia有理数类型继续(ongo)用例学习,制造相对复杂的提升机制用法,提升规则如下:
# 某种整数类型分子分母提升
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)
该短小精悍的提升规则,和数值类型的构造方法、默认convert
方法一起,足以让有理数和Julia别的数值类型(整数/浮点数/复数)完全自然交互。
通过同样方式(in the same manner)提供合适的转换方法和提升规则,任何用户自定义的数值类型都可以和Julia预定义的数值类型易如反掌地交互。
- 注意
convert
和构造方法区别、宽泛类型和超类区别。