回想【函数】,一组值到一个返回值的映射,或抛出异常,如果不能返回合适的值。
相同概念函数或操作对不同参数类型有完全不同的实现是很平常的:两个整数相加和两个浮点数相加是大不一样的,两者的区别就是整型和浮点型参数。
尽管实现不同,这些操作总归是“加”这个公共概念。
相应地,在Julia中,这些行为都取决于一个对象:+
函数。
为了帮助骚年流畅地应用相同概念的众多不同实现,函数绝不须一次定义,而能分段提供确定参数类型和参数个数组合指定行为地定义。 函数的一个可能行为的定义称为方法。 迄今为止,大家仅演练了只定义了一个方法的函数样例,适用全部类型参数。 然而,方法定义的鲜明特征可注解地表明参数类型及参数个数,并非单个方法定义能提供的。 当函数适合特定参数组,最适合某参数组的方法被应用。 因此,函数的全部行为是其各种各样的方法定义的行为的平凑物。 如果这个拼凑物设计得好,即使各个方法得每个实现可能完全不同,函数外在得表现是无痕并一致的。
应用一个函数是选择执行哪个方法叫做分发(重载)。 Julia允许基于参数个数、参数类型地选择最合适的方法。 这跟传统面向对象编程语言不同——这些货只是基于第一个参数分发,通常有特殊参数语法,甚至隐含的而不是明确地写出参数。
在C++或Java中,例如,调用`object.method(argumentAlice, argumentBob)`,`object`接收方法调用并隐含地传递给方法,通过`this`关键字,而不是某个显式的方法参数。当前`this`是方法调用的接收者,它可以一并省略,只写`method(argumentAlice, argumentBob)`,此时`this`暗指接收对象。
根据函数全体参数决定应该调用哪个方法,而不是第一个参数,人称【多重分发】。
多重分发在数学编码中非常有用,在该场合人工认为操作属于某个参数而不属于别的任何参数毫无意义:例如加法操作,x+y
中+
属于x
还是属于y
?
数学操作的实现通常基于全体参数的类型。
甚至超出(抛开)数学操作,不管怎样,多重分发都是(end up)构造和组织程序的强大且便捷的范式。
直到现在,咱曾经在咱的例程中,仅定义过只有一个参数不受约束的方法的函数。
这些函数行为就像在传统动态类型编程语言中一样。
不过,咱几乎可以继续撸多重分发和方法而不用搭理这回事:所有Julia标准函数和操作,像前述的+
函数,有众多方法定义,在各种参数类型和个数的组合上各显神通。
定义一个函数时,可选择地约束适用的参数类型,用::
类型断言操作,在【复合类型】介绍的:
julia> f(x::Float64, y::Float64) = 3x + 4y
f (generic function with 1 method)
该函数定义仅适合x
和y
都是Float64
类型的值:
julia> f(3.0, 4.0)
25.0
任何一个不是Float64
的参数调用都会报MethodError
:
julia> f(9527, 4.0)
ERROR: MethodError: no method matching f(::Int64, ::Float64)
Closest candidates are:
f(::Float64, ::Float64) at REPL[1]:1
Stacktrace:
[1] top-level scope at none:0
julia> f(3.0, 1314)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
Closest candidates are:
f(::Float64, ::Float64) at REPL[1]:1
Stacktrace:
[1] top-level scope at none:0
julia> f(Float32(3.0), Float32(4.0))
ERROR: MethodError: no method matching f(::Float32, ::Float32)
Stacktrace:
[1] top-level scope at none:0
julia> f("3.0", "4.0")
ERROR: MethodError: no method matching f(::String, ::String)
Stacktrace:
[1] top-level scope at none:0
正如卿所见,参数必须是精确的Float64
类型。
别的数值类型,如Float32
和Int
值,不能自动转换成Float64
,或字符串(字面值)解析为数字。
因为Float64
是具体类型,Julia中具体类型不能有子类型,这么定义f
只适用于Float64
类型的参数。
然而编写参数是抽象类型的通用方法经常会有用:
julia> f(x::Number, y::Number) = 3.0x - 4.0y
f (generic function with 2 methods)
julia> f(9527, 1314)
23325.0
该方法定义适用任意一组Number
实例的参数。
不需要是相同类型的,各自可选数值类型。
处理各个数值类型的问题委派给表达式3.0x - 4.0y
的算术操作。
定义多个方法的函数,有朋友简单地以不同参数类型和参数个数多次定义函数。
第一个方法定义为创建该函数对象的函数,之后的方法定义为给该函数对象添加新方法的函数。
当应用该函数时,参数类型和参数个数最匹配的的指定方法定义将被执行。
因此,上述定义的两个方法,强调(重点——敲黑板),定义了所有抽象类型Number
的各对儿实例为参数的f
的行为——但和Float64
类型对儿指定的行为不同。
如果其中一个参数是Float64
但另外一个不是,则不调用f(x::Float64, y::Float64)
,而调用更一般的f(x::Number, y::Number)
方法:
julia> f(2.0, 2.0)
14.0
julia> f(2, 2)
-2.0
julia> f(1+1im, 1+1im)
-1.0 - 1.0im
那个3x - 4y
仅用于f(2.0, 2.0)
的情况,但3.0x - 4.0y
则用于其余场景。
函数参数未执行任何强制转换或提升转换:Julia中所有提升转换都是显式的,而非魔术般。
【转换和提升】,无论如何,展示足够高级的技术和魔法难以区分的应用有多聪明。
对于非数值或更少更多参数,函数f
保留未定义,调用的话仍然报MethodError
错误:
julia> f(2)
ERROR: MethodError: no method matching f(::Int64)
Closest candidates are:
f(::Number, ::Number) at REPL[10]:1
f(::Float64, ::Float64) at REPL[1]:1
Stacktrace:
[1] top-level scope at none:0
julia> f("huaan", "qiuxiang")
ERROR: MethodError: no method matching f(::String, ::String)
Stacktrace:
[1] top-level scope at none:0
julia> f()
ERROR: MethodError: no method matching f()
Closest candidates are:
f(::Float64, ::Float64) at REPL[1]:1
f(::Number, ::Number) at REPL[10]:1
Stacktrace:
[1] top-level scope at none:0
julia> f(2, 250, 9527, 1314)
ERROR: MethodError: no method matching f(::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
f(::Number, ::Number) at REPL[10]:1
Stacktrace:
[1] top-level scope at none:0
在交互式会话中,很容易了解函数对象自身存在多少方法:
julia> f
f (generic function with 2 methods)
该输出告诉咱f
是有两个方法的函数。
想找出这些方法的签名,用methods
函数:
julia> methods(f)
# 2 methods for generic function "f":
[1] f(x::Float64, y::Float64) in Main at REPL[1]:1
[2] f(x::Number, y::Number) in Main at REPL[10]:1
展示了这两个方法的具体模式:一个带两个Float64
参数、一个带两个Number
参数。
同时也表明各个方法定义所在的文件及其行号:因为这些方法在JuliaREPL中定义,得到的表面上行号是REPL[n]:1
。
缺乏以::
的类型声明,方法参数类型默认Any
,意味着参数类型无约束,因为Julia中所有值都是抽象类型Any
的实例。
因此,可以定义捕捉所有参数类型的f
符下:
julia> f(x, y) = println("Whoa there, Nelly.")
f (generic function with 1 method)
julia> f("alice", "bob")
Whoa there, Nelly.
该捕获全部的版本不如任何别的可能为具体类型参数值对儿定义的方法特殊,所以仅当没有更合适的方法可调用时才用此版本。
尽管似乎是简单的概念,根据值得类型多重分发几乎是Julia编程语言唯一最强大最核心的特色。 核心操作通常有一堆方法:
julia> methods(+)
# 163 methods for generic function "+":
...
julia> methods(-)
# 175 methods for generic function "-":
...
配合Julia提供的灵活的带参数的类型系统多重分发,可与实现细节解耦地、抽象地表达高级算法,也能生成高效地、专门的代码在运行时处理各种情况。
有这种可能:定义的函数方法集合,对于某种参数(类型)组合,有不止一个最适合的方法。
julia> g(x::Float64, y) = 3x + 4y
g (generic function with 1 method)
julia> g(x, y::Float64) = 9527x + 1314y
g (generic function with 2 methods)
julia> g(2.5, 2)
15.5
julia> g(2, 2.5)
22339.0
julia> g(1.0, 1.0)
ERROR: MethodError: g(::Float64, ::Float64) is ambiguous. Candidates:
g(x, y::Float64) in Main at REPL[2]:1
g(x::Float64, y) in Main at REPL[1]:1
Possible fix, define
g(::Float64, ::Float64)
Stacktrace:
[1] top-level scope at none:0
调用g(1.0, 1.0)
可以被g(Float64, Any)
方法或g(Any, Float64)
方法处理,且并没有哪个方法比另一个更适合。
这种情况下,Julia抛出MethodError
而不是任意选用一个方法。
可通过指定合适的方法来避免“岔路口”的方法歧义。
julia> g(x::Float64, y::Float64) = x + y
g (generic function with 3 methods)
julia> g(1.0, 1.0)
2.0
推荐事先定义好消除歧义的方法,因为不这样做的话就会存在歧义,若是暂时性得,可待需要定义更适合得方法时处理。
更多复杂得场景中,解决方法歧义牵扯设计的确定元素;这个话题在【方法定义歧义】(下边)探讨。
方法定义有可选地类型参数来强化签名。
julia> sametype(x::T, y::T) where {T} = true
sametype (generic function with 1 method)
julia> sametype(x, y) = false
sametype (generic function with 2 methods)
第一个方法适用任何两个参数是相同具体类型地情况,不论具体类型如何; 第二个方法是捕捉一切担当,覆盖全部场景。 因此,总的来说,上述定义是检查两个参数类型相同否的布尔函数。
julia> sametype(9527, 1314)
true
julia> sametype(9527.0, 1314)
false
julia> sametype(9527.0, 1314.0)
true
julia> sametype("9527.0", 1314.0)
false
julia> sametype("huaan", 1314.0)
false
julia> sametype("huaan", "qiuxiang")
true
julia> sametype(Int32(9527), Int64(1314))
false
如此定义,相当于类型签名是UnionAll
类型(【类型】)的方法簇。
这种函数定义重载行为十分平常、惯用,即使在Julia中。
方法类型参数不限于当作参数的类型使用:可用在函数体或函数签名中的值能出现的任何地方。
这儿给个例子:方法类型参数T
用作带参数的类型Vector{T}
的方法签名的类型参数。
julia> joyappend(v::Vector{T}, x::T) where {T} = [v..., x]
joyappend (generic function with 1 method)
julia> joyappend([0,1,2,9527,1314], 250)
6-element Array{Int64,1}:
0
1
2
9527
1314
250
julia> joyappend([0,1,2,9527,1314], 2.5)
ERROR: MethodError: no method matching joyappend(::Array{Int64,1}, ::Float64)
Closest candidates are:
joyappend(::Array{T,1}, ::T) where T at REPL[17]:1
Stacktrace:
[1] top-level scope at none:0
julia> joyappend([0.0,1.0,2.0,9527.0,1314.0], 2.5)
6-element Array{Float64,1}:
0.0
1.0
2.0
9527.0
1314.0
2.5
julia> joyappend([0.0,1.0,2.0,9527.0,1314.0], 250)
ERROR: MethodError: no method matching joyappend(::Array{Float64,1}, ::Int64)
Closest candidates are:
joyappend(::Array{T,1}, ::T) where T at REPL[17]:1
Stacktrace:
[1] top-level scope at none:0
可见,被添加的元素必须符合要添加到的向量元素类型,否则MethodError
伺候。
下面的样例中,方法类型参数T
用作返回值。
julia> joytypeof(x::T) where {T} = T
joytypeof (generic function with 1 method)
julia> joytypeof(9527)
Int64
julia> joytypeof(2.5)
Float64
正如可以给类型声明中添加参数类型的子类型约束,也可以约束方法参数类型。
julia> sametype!numberic(x::T, y::T) where {T<:Number} = true
sametype!numberic (generic function with 1 method)
julia> sametype!numberic(x::Number, y::Number) = false
sametype!numberic (generic function with 2 methods)
julia> sametype!numberic(9527, 1314)
true
julia> sametype!numberic(9527, 2.5)
false
julia> sametype!numberic(2.0, 2.5)
true
julia> sametype!numberic("wangcai", 2.5)
ERROR: MethodError: no method matching sametype!numberic(::String, ::Float64)
Closest candidates are:
sametype!numberic(::T<:Number, ::T<:Number) where T<:Number at REPL[27]:1
sametype!numberic(::Number, ::Number) at REPL[28]:1
Stacktrace:
[1] top-level scope at none:0
julia> sametype!numberic("wangcai", "xiaoqiang")
ERROR: MethodError: no method matching sametype!numberic(::String, ::String)
Stacktrace:
[1] top-level scope at none:0
函数sametype!numberic
行为类似上边定义的sametype
,但仅适用于一双数值参数。
带参数的方法允许同【UnionAll
】类型中相同的where
表达式语法来写类型。
如果仅有一个参数,where {T}
中的花括号可省略,但清晰起见,经常是推荐。
多个参数可用都好分隔,或嵌套where
,如where S<:Real where T
。
当重定义方法或添加方法,认识到有些变化不会立即生效是很重要的。
这是Julia没有通常的JIT技巧和开销,却能能够静态推断、编译代码快速执行的关键。
的确,任何新方法定义,当前运行环境不可见,包括任务和线程(以及任何早先定义的@generated
函数)。
一起看个例子来领会真意:
julia> function tryeval()
@eval newf() = 9527
newf()
end
tryeval (generic function with 1 method)
julia> tryeval()
ERROR: MethodError: no method matching newf()
The applicable method may be too new: running in world age 25037, while current world is 25038.
Closest candidates are:
newf() at REPL[34]:2 (method too new to be called from this world context.)
Stacktrace:
[1] tryeval() at .\REPL[34]:3
[2] top-level scope at none:0
julia> newf()
9527
这个例子中,观察到新定义的newf
已经被创建,但不能立即调用。
新的tryeval
函数全局可见,因此可以写return newf
(无须插入语(parentheses))。
但不论码农,或者别的上级主调者,抑或别的相关函数,都不能调用该新定义的方法。
但有个例外:自JuliaREPL中以将来(future)调用newf
可如期工作,对新定义的newf
即可见又可调用。
然而,将来调用tryeval
将继续将newf
的定义看作JuliaREPL之前的声明,因此是早于调用newf
的。
可能群众想自行一探究竟。
该行为实现是“世界年龄计数(纪元)”。
该计数单调递增地跟踪每次方法定义操作。
这允许以单个数字(纪元)描述“给定运行环境可见的方法定义集合”。
这也允许通过比较序号来比较两个“世界”中可用的方法。
上上述例子中,大家看到“当前世界”,即newf
所存在的环境,比开始执行tryeval
时就固定的任务本地“运行世界”的纪元大“一岁”。
有些时候,有必要应付它(例如用户实现上述JuliaREPL)。
幸运的是,有个简单的解决方法:用Base.invokelatest
调用函数。
julia> function tryevalya()
@eval newfya() = 1314
Base.invokelatest(newfya)
end
tryevalya (generic function with 1 method)
julia> tryevalya()
1314
最后,一起看个更复杂的该规则起作用的(come into play)例子。定义有初始时有一个方法的的函数f(x)
:
julia> f(x) = "xiucai loves poland."
f (generic function with 1 method)
开始一些别的用到f(x)
的操作:
julia> g(x) = f(x)
g (generic function with 4 methods)
julia> t = @async f(wait()); yield();
现在给f(x)
添加某些新方法:
julia> f(x::Int) = "xiucai loves julia."
f (generic function with 2 methods)
julia> f(x::Type{Int}) = "xiucai loves llvm."
f (generic function with 3 methods)
对比以下结果为何不同:
julia> f(0)
"xiucai loves julia."
julia> g(0)
"xiucai loves julia."
julia> fetch(schedule(t, 0))
"xiucai loves poland."
julia> t = @async f(wait()); yield();
julia> fetch(schedule(t, 0))
"xiucai loves julia."
TODO: 为何?
由于复杂重载逻辑不是性能或可用性必须的,有些时候,是参数一些算法最好的方式。 当这样重载时候,偶尔提出一些常用设计模式。
下面是返回任何AbstractArray
子类型T
的元素类型的正确代码模板:
abstract type AbstractArray{T, N} end
eltype(::Type{<:AbstractArray{T}}) where {T} = T
采用所谓的“三角重载(triangular dispatch)”。
注意T
是UnionAll
类型,如eltype(Array{T} where T <: Integer)
,则返回Any
(如Base
中eltype
版本一样)。
TODO: 需要细细理解。
另一种方式,在Julia v0.6引入三角重载之前唯一正确的方式:
abstract type AbstractArray{T, N} end
eltype(::Type{AbstractArray}) = Any
eltype(::Type{AbstractArray{T}}) where {T} = T
eltype(::Type{AbstractArray{T, N}}) where {T, N} = T
eltype(::Type{A}) where {A<:AbstractArray} = eltype(supertype(A))
还有种可能如下,在要求参数T
更严密地匹配地场景很有用:
eltype(::Type{AbstractArray{T, N} where {T<:S, N<:M}}) where {M, S} = Any
eltype(::Type{AbstractArray{T, N} where {T<:S}}) where {N, S} = Any
eltype(::Type{AbstractArray{T, N} where {N<:M}}) where {M, T} = T
eltype(::Type{AbstractArray{T, N}}) where {T, N} = T
eltype(::Type{A}) where {A <: AbstractArray} = eltype(supertype(A))
一个常见错误就是尝试通过内省(introspection)获取元素类型:
julia> eltypewrong(::Type{A}) where {A<:AbstractArray} = A.parameters[1]
eltypewrong (generic function with 1 method)
可是,构造反例并不难:
julia> struct BitVectorYA <: AbstractArray{Bool, 1}; end
创建了BitVectorYA
类型,没有参数,然而元素类型任然被充分指明,T
等于Bool
。
在构建一般代码过程中,通常需要构造近似对象,如类型布局的某些变化,类型参数的变化也是必要的。
举个例子,有一组元素类型任意的抽象数组,想以指定元素类型撸码计算。
就必须针对每种AbstractArray{T}
子类型实现方法描述如何计算该子类型转换。
没有根据不同参数转换一个子类型到另一个子类型的一般方法(快速回顾这事为什么)。
AbstractArray
的子类型典型地实现两个方法来达到一般转换的目的:
一个方法把输入数组转换为指定AbstractArray{T, N}
的子类型;
一个方法创建指定元素类型的未初始化的新数组。
实现样例在Julia的Base
中可见。
这里演示基本使用,确保input
和output
类型相同:
input = convert(AbstractArray{Eltype}, input)
output = similar(input, Eltype)
作为扩展,在需要输入数组拷贝的算法中,convert
不能胜任,返回值是原始input
的别名。
结合similar
(制造输出数组)和copyto!
(以输入数据填充)是表达输入参数的可变拷贝所需的一般方式。
copy!with!eltype(input, Eltype) = copyto!(similar(input, Eltype), input)
为了重载多级参量化的参数列表,分离每个重载层级到不同函数通常是最好的。 这可能听起来和单重载方法类似,但大家接下来可看到,这任然很灵活。
例如,尝试根据数组元素类型重载通常将走火入魔(run into ambiguous situations)。 相反,常用代码会先根据容器类型重载,然后根据元素类型递归到更合适的方法。 在更多场景中,算法把自身方便地借出给该层级方法,然而别的情况下,该严厉(限制)必须人工解决。 重载分支明确可见,例如两个矩阵求和:
# 首先重载选择map算法逐元素地求和
+(a::Matrix, b::Matrix) = map(+, a, b)
# 然后重载处理每个元素并选择合适得常用元素类型来计算
+(a, b) = +(promote(a, b)...)
# 最后一旦元素类型相同则相加
# 如通过处理器暴露的原始操作
+(a::Float64, b::Float64) = Core.add(a, b)
上述迭代重载的一种自然扩展是给方法选择添加一层,允许根据和类型层级定义的类型集合无关的类型集合重载。
可以誊写问题中类型的Union
来构造这样的类型集合,但是该类型集合一旦创建后,不能像Union
类型那样被扩展、不能更改。
然而,该扩展类型集合可以通常参考作holy-trait的设计模式编程。
TODO: 详解holy-trait
含义。
该模式通过给函数参数所属的每个特征集合定义一个计算不同单例值或类型的一般函数。 如果该函数是存粹的、相比正常重载,对性能没有影响。
上边的例子掩盖了map
和promote
的实现细节,这些函数均根据这些特征操作。
当迭代矩阵时,如map
所实现的,一个重要的问题是“横贯数据的顺序是什么”。
当AbstractArray
子类型实现Base.IndexStyle
特征,别的函数,如map
可根据该信息重载,选择最优算法(【抽象数组接口】)。
这意味着,不必为每种子类型都实现一个定制的map
版本,因为一般定义和特征类将能让系统选择最快速版本。
这儿展示一个map
的玩具实现,阐明基于特征重载:
map(f, a::AbstractArray, b::AbstractArray) = map(Base.IndexStyle(a, b), f, a, b)
# 一般实现
map(::Base.IndexCartesian, f, a::AbstractArray, b::AbstractArray) = ...
# 线性索引实现(快速)
map(::Base.IndexLinear, f, a::AbstractArray, b::AbstractArray) = ...
基于特征的方式也出现在promote
机制,用在标量相加(+
)。
它采用promote_type
返回最佳的常用类型来计算被给出操作数的两个类型的操作。
这让减少为每种可选的类型参数对实现函数成为可能,添加一个优先的逐对提升规则表。
基于特征提升的讨论为下一种设计模式提供了过渡(引子):计算矩阵操作输出元素的类型。
对于实现原始操作,如加法,采用promote_type
函数计算要求的输出类型。
如前,在调用+
中的promote
调用的工作中可见。
对于矩阵上更复杂的函数,计算更复杂操作序列期望的返回类型也许是有必要的。 通常按以下步骤执行:
- 编写小函数
op
表达算法核心所执行的一组操作; - 依
promote_op(op, argument_types...)
计算结果矩阵的元素类型R
,这里的argument_types
计算自作用在每个输入数组的eltype
; - 照
similar(R, dims)
构建输出矩阵,这里的dims
是输出数组要求的维度。
至于更多特定例程,一个一般的方阵乘法伪代码看起来如下:
function matmul(a::AbstractMatrix, b::AbstractMatrix)
op = (ai, bi) -> ai * bi + ai * bi
## 这是无效的,因其假定`one(eltype(a))`是可构造的:
# R = typeof(op(one(eltype(a)), one(eltype(b))))
## 这会失败,因其假定`a[1]`存在且代表数组中所有元素的类型:
# R = typeof(op(a[1], b[1]))
## 这是不对的,因其假定`+`调用`promote_type`但这对某些类型是不正确的,比如布尔类型:
# R = promote_type(ai, bi)
## 这是错误的,因其所依赖的类型推断的返回值非常脆弱(也不是可优化的):
# R = Base.return_types(op, (eltype(a), eltype(b)))
## 但是,最后这个,它起作用:
R = promote_op(op, eltype(a), eltype(b))
## 尽管有些时候它可能给出超出要求的“大类型”,它总是给出正确类型。
output = similar(b, R, (size(a, 1), size(b, 2)))
if size(a, 2) > 0
for j in 1:size(b, 2)
for i in 1:size(b, 1)
## here we don't use `ab = zero(R)`,
## 这里咱不用`ab = zero(R)`,
## 因为`R`应为`Any`且`zero(Any)`未定义,
## 咱也必须声明`ab::R`以让`ab`的类型在循环中不变,
## 因为可能`typeof(a * b) != typeof(a * b + a * b) == R`
ab::R = a[i, 1] * b[1, j]
for k in 2:size(a, 2)
ab += a[i, k] * b[k, j]
end
output[i, j] = ab
end
end
end
return output
end
显著削减编译时间和测试复杂度的一个途径是分离转换为要求类型的逻辑和计算。 这让编译器独立于更大核心体的剩余部分来特殊化和内联转换逻辑。
这是从较大类型级别向某个算法实际支持的指定参数类型转换时常见的模式:
complexfunction(arg::Int) = ...
complexfunction(arg::Any) = complexfunction(convert(Int, arg))
matmul(a::T, b::T) = ...
matmul(a, b) = matmul(promote(a, b)...)
应用到一个【不定参数函数】的参数个数也可以被限制。
注解Vararg{T,N}
用来指示该限制。例如:
julia> bar(a,b,etc::Vararg{Any,2}) = (a,b,etc)
bar (generic function with 1 method)
julia> bar(0,1,2)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any) at REPL[1]:1
Stacktrace:
[1] top-level scope at none:0
julia> bar(0,1,2,250)
(0, 1, (2, 250))
julia> bar(0,1,2,250,9527,1314)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any) at REPL[1]:1
Stacktrace:
[1] top-level scope at none:0
更有用地,可以通过一个参数限制不定参数方法。例如:
function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}
仅当indices
数量匹配数组维度时可调用。
仅当提供的参数类型需要被Vararg{T}
限制时可等价地写为T...
。
例如f(x::Int...) = x
是f(x::Vararg{Int}) = x
的速记法。
如【函数】简要提到的,可选参数作为多个方法定义的语法实现。如下定义:
julia> f(a=9527, b=1314) = a + 2b
f (generic function with 3 methods)
翻译为如下三个函数:
julia> f(a, b) = a + 2b
f (generic function with 3 methods)
julia> f(a) = f(a, 1314)
f (generic function with 3 methods)
julia> f() = f(9527, 1314)
f (generic function with 3 methods)
试试f(a, b) = f(a, b); f(0, 1)
会如何!
意味着,调用f()
相当于调用f(9527, 1314)
。
这种情况下,结果是12155
,因为f(9527, 1314)
调用上述f
的第一个方法。
然而,不必总是这种情况。
如果定义整型更合适的第四个方法:
julia> f(a::Int, b::Int) = a - 2b
f (generic function with 4 methods)
julia> f(9527, 1314)
6899
julia> f()
6899
若此,f(9527, 1314)
和f()
的结果均为6899
。
换句话说,可选参数是绑定在函数的,而不是该函数特定的方法!
根据可选参数的类型决定调用哪个方法。
当可选参数按照全局变量被定义,可选参数类型甚至可在运行时发生改变。
关键字参数的行为和普通位置参数迥异。 特别是,关键字参数不参与方法重载。 方法仅基于位置参数(的类型和个数(可选参数))重载,关键字参数在匹配的方法验明正身后被处理。
方法和类型关联,因此通过添加方法到它的类型制造任意可调用的Julia对象是可能的。 这种可调用的对象某些时候称作“函子(functor)”。
举个例子,可以定义保存多项式相关系数的类型,但像函数一样计算多项式:
julia> struct Polynomial{R}
coeffs::Vector{R}
end
julia> function (p::Polynomial)(x)
v = p.coeffs[end]
for i = (length(p.coeffs)-1):-1:1
v = v*x + p.coeffs[i]
end
return v
end
julia> (p::Polynomial)() = p(3)
注意,该函数由类型指定,而不是由名称指定。
像常规函数一样,由简洁语法形式。
在函数体中,p
将引用所调用的对象。
一个Polynomial
可如下使用:
julia> p = Polynomial([1,9,250])
Polynomial{Int64}([1, 9, 250])
julia> p(5)
6296
julia> p(3)
2278
julia> p()
2278
该机制对类型构造函数和闭包(内部函数引用其周遭环境)在Julia中如何工作也至关重要!
偶尔,引入不带方法的函数也是有用的。
可用于区分接口定义和实现。
也可出于文档或代码可读性目的而作。
空函数的语法就是function
块不带参数列表:
julia> function emptyegg
end
emptyegg (generic function with 0 methods)
猪吏的方法多态性是其最强大的特性之一,利用这种能力可造成设计挑战。 特别是,在更复杂的方法层级中,产生歧义并不稀奇。
上边,指出可以如下解决方法歧义:
f(x, y::Int) = 9527
f(x::Int, y) = 1314
这通常是正确的策略;然而,盲从该建议会适得其反。 特别是,函数拥有越多方法,越可能产生歧义。 当方法层级比上述简单例子更难懂的时候,花点儿时间仔细考虑别的策略是值得的。
下面就讨论特殊挑战和一些解决方法歧义的可选方法。
Tuple
(和NTuple
)参数表达特殊挑战,例如:
f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2
因为N == 0
的可能而导致歧义:没有元素用来决定是Int
还是Float64
变体应该被调用。
解决该歧义,一种方法是定义空元组方法:
f(x::Tuple{}) = 9527
此外,所有方法可以强调元组中至少要有一个参数:
f(x::NTuple{N,Int}) where {N} = 1 # 这是退路
f(x::Tuple{Float64, Vararg{Float64}}) = 2 # 这要求元组中至少一个Float64的元素
当可能冒险在两个或更多参数重载方法时,考虑可给较简单的设计制作一个包装函数否。 例如,不要编写多个变体:
f(x::A, y::A) = ...
f(x::A, y::B) = ...
f(x::B, y::A) = ...
f(x::B, y::B) = ...
而要考虑定义:
f(x::A, y::A) = ...
f(x, y) = f(g(x), g(y))
这里的g
转换参数类型为A
。
这是更一般正交设计原则非常有代表性的例子,不同的方法分配不同的概念。
这里g
很可能需要退路定义:g(x::A) = x
。
一个相关的策略是拓展promote
恢复x
和y
为共同类型:
f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)
这种设计可能的风险是没有合适的提升方法转换x
和y
为相同类型,第二个方法会无穷递归自身并除法栈溢出。
非扩展的函数Base.promote_noncircular
做作为备选;当提升失败风然抛出错误,但有一点,更快地以失败告特定错误信息。
如果需要在多个参数上重载方法,要达到实用性,需要定义全部可能的变体,会有太多组合退路要准备,此处考虑引入“命名瀑布”,在第一个参数重载,接着调用内部方法。
f(x::A, y) = _fA(x, y)
f(x::B, y) = _fB(x, y)
接着内部方法_fA
和_fB
可根据y
重载,不涉及有关x
的相互歧义。
要意识到这个策略至少有一个主要缺陷:在很多场景,用户通过定义暴露的函数f
的更严密版本来深度定制f
的行为是可行的,用户必须更精确定义内部方法_fA
和_fB
,这污染暴露方法和内部方法之间的代码行。
可能的话,尝试避免定义根据抽象容器的指定元素类型重载的方法。例如:
-(A::AbstractArray{T}, b::Date) where {T<:Date}
任何用户定义下面方法则产生歧义:
-(A::MyArrayType{T}, b::T) where {T}
最好的方法是避免定义这些方法的任何一个,取而代之地,依靠一个普通方法-(A::AbstractArray, b)
并并确保该方法以一般调用(如similar
和-
)实现,分别正确处理不同类型的容器和元素。
这只是建议【正交化】方法定义的一个稍微复杂的变体。
当这种方法不可行时,开始和别的开发人员讨论解决歧义是有意义的;仅仅因为首先定义了一个不必要的方法,不能修改或不能淘汰。 万不得已(as a last resort)开发人员可以定义“创可贴”方法:
-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...
这将不理性的强制解决歧义。
如果定义方法瀑布提供默认参数,务必小心遗漏任何参数对应的潜在默认值。 例如,假设用户编写数字过滤算法,通过填补处理信号边界:
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel) # 此时执行真的计算
end
这会和提供默认填补的发生冲突:
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # 复制默认填补
这两个方法一起产生无限递归A
不断变大。
好的设计应该如下定义调用层级:
struct NoPad end # 表明不需要任何填补(除非已经应用的)
myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # 默认边界条件
function myfilter(A, kernel, ::Replicate)
Apadded = replicate_edges(A, size(kernel))
myfilter(Apadded, kernel, NoPad()) # 表明新的边界条件
end
# 其余填补方法请走这边
function myfilter(A, kernel, ::NoPad)
# 此处是核心计算的真正实现
end
NoPad
像别的任何填补一样放在相同的参数位置,可以保持好重载层级组织并降低歧义可能性。
此外,它扩展了myfilter
接口:要显式控制填补的用户可以直接调用NoPad
变体。
- 真正强大且容易混乱的东西终于出现,感觉像C++的模板。