方法
章中所述 Functions,函数是一个对象,它将参数元组映射到返回值,或者在无法返回相应值时引发异常。 通常,对于不同类型的参数,相同的概念函数或操作的实现方式完全不同:添加两个整数与添加两个浮点数显着不同,并且这两个原则都不同于添加一个 尽管在实施方面存在差异,但所有这些操作都属于"加法"的一般概念。 因此,在Julia中,所有这些行为都属于单个对象,即'+'函数。
为了简化相同概念的不同实现的使用,不必一次定义所有功能。 它们可以部分设置,为参数类型及其数量的某些组合指定特定行为。 函数的一个可能选项的定义称为_method_。 到目前为止,只给出了使用适用于所有类型参数的单个方法定义的函数的示例。 但是,可以对方法定义签名进行注释,以指定参数的类型以及参数的数量。 可以提供多个方法定义。 当函数应用于特定的参数元组时,将使用适用于这些参数的最具体的方法。 因此,函数的一般行为是其方法的各种定义的一组行为。 当这个集合设计得很好时,即使方法的实现可能完全不同,函数的外部行为也会看起来平滑一致。
应用函数时要执行的方法的选择称为_controlling_。 在Julia中,调度过程可以根据提供的参数数量和所有函数参数的类型来选择调用哪个函数方法。 这与传统的面向对象语言形成鲜明对比,传统的面向对象语言只根据第一个参数执行调度,第一个参数通常具有特殊的参数语法,有时是隐含的,而不是显 脚注:1[例如,在C++ 或者Java,在像`obj这样的方法调用中。meth(arg1,arg2)`,obj对象"接收"方法调用,并通过`this`关键字隐式传递给方法,而不是作为显式方法参数。 当当前对象是’this`时,它是方法调用的接收者,并且可以通过只写`meth(arg1,arg2)`来完全省略它,其中`this`表示接收对象。]使用函数的所有参数来选择要调用的方法,而不仅仅是第一个,被调用https://en.wikipedia.org/wiki/Multiple_dispatch [多重派遣]。 多重分派对于数学代码特别有用,其中人为地假设操作"属于"一个参数而不是任何其他参数是没有意义的:`x+y`中的加法操作是否属于`x`而不是`y'? 数学运算符的实现通常取决于其所有参数的类型。 然而,即使超越数学运算,多重调度也正在成为构建和组织程序的强大而方便的概念。
本章中的所有示例都假定函数的方法是在同一个模块中定义的。 要将方法添加到另一个模块中的函数,您必须使用"import"导入它或使用补充有模块名称的名称。 请参阅有关 命名空间管理。 |
定义方法
到目前为止,在示例中,我们只定义了使用具有无限参数类型的单个方法的函数。 这些函数的操作方式与传统的动态类型语言相同。 尽管如此,我们几乎经常使用多个调度和方法,而没有意识到这一点:Julia中的所有标准函数和运算符,例如前面提到的`+`函数,都有许多方法来定义它们的行
在定义函数时,您可以使用type语句运算符`::`进一步限制它所应用的参数类型。 复合类型:
julia> f(x::Float64, y::Float64) = 2x + y
f (generic function with 1 method)
此函数定义仅适用于其中`x`和`y`是类型值的调用。 '浮64`:
julia> f(2.0, 3.0)
7.0
她将它应用于任何其他类型的参数将导致 'MethodError':
julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(::Float64, !Matched::Float64)
@ Main none:1
[医]堆垛机:
[...]
朱莉娅>f(Float32(2.0),3.0)
错误:MethodError:no method matching f(::Float32,::Float64)
函数'f'存在,但没有为这种参数类型的组合定义方法。
最接近的候选人是:
f(!匹配::Float64,::Float64)
@主要无:1
[医]堆垛机:
[...]
朱莉娅>f(2.0,"3.0")
错误:MethodError:没有匹配f(::Float64,::String)的方法
函数'f'存在,但没有为这种参数类型的组合定义方法。
最接近的候选人是:
f(::Float64,!匹配::Float64)
@主要无:1
[医]堆垛机:
[...]
julia>f("2.0","3.0")
错误:MethodError:没有匹配f(::String,::String)的方法
函数'f'存在,但没有为这种参数类型的组合定义任何方法。
如您所见,参数必须是类型 'Float64'。 其他数字类型(如整数或32位浮点值)不会自动转换为64位浮点,也不会将字符串解析为数字。 由于’Float64’是一个特定的类型,并且Julia中的特定类型不能被子类化,所以这个定义只能应用于完全类型为`Float64’的参数。 但是,通常建议编写更通用的方法,其中声明的参数类型是抽象的。:
julia> f(x::Number, y::Number) = 2x - y
f (generic function with 2 methods)
julia> f(2.0, 3)
1.0
此方法定义适用于作为该类型实例的任何参数对 '数字'。 它们不一定必须具有相同的类型,只要它们是数值即可。 处理异构数字类型的任务委托给表达式`2x-y’中的算术运算。
要用多个方法定义一个函数,用不同数量和类型的参数多次定义该函数就足够了。 第一次定义方法时,会为函数创建函数对象,随后的方法定义会向现有函数对象添加新方法。 当应用函数时,将执行最具体的方法定义,对应于参数的数量和类型。 因此,综合起来,上面的两个方法定义为抽象类型"Number"的所有实例对定义了"f"的行为,但具有特定于值对的不同行为。 'Float64'。 如果其中一个参数是64位浮点而另一个不是,则不能调用`f(Float64,Float64)`方法,应该使用更通用的`F(Number,Number)'方法。:
julia> f(2.0, 3.0)
7.0
julia> f(2, 3.0)
1.0
julia> f(2.0, 3)
1.0
朱莉娅>f(2,3)
1
仅在第一种情况下使用'2x+y`的定义,其余情况下使用`2x-y’的定义。 函数参数的自动转换或转换永远不会执行:Julia中的所有转换都不是特别的东西-它们以完全显式的方式进行。 然而,在章 转化和提升显示了如何巧妙地应用足够先进的技术,可以从魔术无法区分。 脚注:Clarke61[Arthur C.Clarke,Profiles Of The Future("Features Of The Future")(1961):Clark’S Third Law.]
对于非数字值,以及数量少于或多于两个的参数,函数’f’保持未定义,其应用仍然会导致 '方法错误`:
julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Number, ::Number)
@ Main none:1
f(!Matched::Float64, !Matched::Float64)
@ Main none:1
Stacktrace:
[...]
julia> f()
ERROR: MethodError: no method matching f()
The function `f` exists, but no method is defined for this combination of argument types.
Closest candidates are:
f(!Matched::Float64, !Matched::Float64)
@ Main none:1
f(!Matched::Number, !Matched::Number)
@ Main none:1
Stacktrace:
[...]
要查看函数存在哪些方法,请在交互式会话中输入函数对象本身。:
julia> f
f (generic function with 2 methods)
从这个输出中可以清楚地看出,'f’是一个具有两个方法的函数对象。 要找出这些方法的签名,请使用函数 '方法':
julia> methods(f)
# Два метода для универсальной функции f из Main:
[1] f(x::Float64, y::Float64)
@ none:1
[2] f(x::Number, y::Number)
@ none:1
这表明’f’有两个方法,其中一个接受两个`Float64`的参数,另一个-类型为`Number’的参数。 它还指定了定义方法的文件和行号:由于这些方法是在REPL中定义的,所以明显的行号是`none:1'。
如果没有使用`::的类型声明,则方法的默认参数类型是’Any`,这意味着它没有限制,因为Julia中的所有值都是抽象类型’Any’的实例。 因此,可以为`f`定义一个通用方法,如下所示:
julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)
朱莉娅>方法(f)
#Main中通用函数f的三种方法:
[1]f(x::Float64,y::Float64)
@无:1
[2]f(x::数,y::数)
@无:1
[3]f(x,y)
@无:1
julia>f("foo",1)
哇,耐莉。
对于一对参数值,此通用方法比任何其他可能的方法定义都不那么具体,因此只会对那些没有其他方法定义适用的参数对调用它。
请注意,类型没有在参数`x`和`y`的第三个方法的签名中指定。 这是表示`f(x::Any,y::Any)`的缩写方式。
尽管它表面上很简单,但对值类型的多重调度也许是Julia语言中最强大和最主要的特性。 基本操作通常有几十种方法。:
julia> methods(+)
# 180 методов для универсальной функции «+»:
[1] +(x::Bool, z::Complex{Bool}) in Base at complex.jl:227
[2] +(x::Bool, y::Bool) in Base at bool.jl:89
[3] +(x::Bool) in Base at bool.jl:86
[4] +(x::Bool, y::T) where T<:AbstractFloat in Base at bool.jl:96
[5] +(x::Bool, z::Complex) in Base at complex.jl:234
[6] +(a::Float16, b::Float16) in Base at float.jl:373
[7] +(x::Float32, y::Float32) in Base at float.jl:375
[8] +(x::Float64, y::Float64) in Base at float.jl:376
[9] +(z::Complex{Bool}, x::Bool) in Base at complex.jl:228
[10] +(z::Complex{Bool}, x::Real) in Base at complex.jl:242
[11] +(x::Char, y::Integer) in Base at char.jl:40
[12] +(c::BigInt, x::BigFloat) in Base.MPFR at mpfr.jl:307
[13] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt, e::BigInt) in Base.GMP at gmp.jl:392
[14] +(a::BigInt, b::BigInt, c::BigInt, d::BigInt) in Base.GMP at gmp.jl:391
[15] +(a::BigInt, b::BigInt, c::BigInt) in Base.GMP at gmp.jl:390
[16] +(x::BigInt, y::BigInt) in Base.GMP at gmp.jl:361
[17] +(x::BigInt, c::Union{UInt16, UInt32, UInt64, UInt8}) in Base.GMP at gmp.jl:398
...
[180] +(a, b, c, xs...) in Base at operators.jl:424
由于多次调度,以及Julia中灵活的参数类型系统,高级算法可以抽象地表达,与实现细节分开。
方法的专业化
如果创建了同一函数的多个方法,这有时被称为"特化"。 在这种情况下,通过添加额外的方法来专门化函数:每个新方法都是函数的另一种专门化。 如上所示,特化由`methods`函数返回。
还有另一种类型的特化在没有程序员干预的情况下发生:Julia编译器可以自动为所使用的参数类型专门化方法。 这样的特化不包括在`methods`函数输出的列表中,因此不会创建新方法(Method
)。 但是,可以使用以下工具查看它们 '@code_typed'。
例如,在创建方法时
mysum(x::Real, y::Real) = x + y
'Mysum’函数将接收一个新方法(可能是唯一一个),该方法接受任何一对`Real`类型作为输入。 但如果你执行
julia> mysum(1, 2)
3
julia> mysum(1.0, 2.0)
3.0
Julia编译`mysum`两次:一次为`x::Int,y::Int`,再次为`x::Float64,y::Float64`。 双重编译的要点是提高性能:调用`+`(并由`mysum`使用)的方法因类型`x`和`y`而异,并且通过在Julia中编译不同的专业化,可以提前搜索方法。 这允许程序运行得更快,因为它不必在执行期间搜索方法。 Julia中的自动特化允许您编写通用算法,期望编译器为每个必要的情况生成高效,专门的代码。
在可能的特化数量可能无限的情况下,这种默认特化机制可能不会在Julia中应用。 有关详细信息,请参阅 请记住,当Julia中的专业化不是自动完成的时候。
方法的模糊性
一组函数方法可以以这样一种方式定义,即没有适用于参数的某些组合的唯一最具体的方法。:
julia> g(x::Float64, y) = 2x + y
g (generic function with 1 method)
julia> g(x, y::Float64) = x + 2y
g (generic function with 2 methods)
julia> g(2.0, 3)
7.0
julia> g(2, 3.0)
8.0
julia> g(2.0, 3.0)
ERROR: MethodError: g(::Float64, ::Float64) is ambiguous.
Candidates:
g(x, y::Float64)
@ Main none:1
g(x::Float64, y)
@ Main none:1
Possible fix, define
g(::Float64, ::Float64)
Stacktrace:
[...]
在这里,对`g(2.0,3.0)`的调用可以通过`g(::Float64,::Any)`方法或`g(::Any,::Float64)`方法来处理。 定义方法的顺序并不重要,也没有比其他方法更具体。 在这种情况下,朱莉娅问题 'MethodError',而不是任意选择方法。 为了消除方法的模糊性,您需要在交集的情况下指定适当的方法。:
julia> g(x::Float64, y::Float64) = 2x + 2y
g (generic function with 3 methods)
julia> g(2.0, 3)
7.0
朱莉娅>g(2,3.0)
8.0
朱莉娅>g(2.0,3.0)
10.0
建议首先定义一个明确的方法,因为否则会有模糊性,尽管是暂时的。 它将一直存在,直到确定更具体的方法。
在更复杂的情况下,需要一定的设计元素来解决方法的模糊性。 这个问题正在考虑中 下面。
参数化方法
方法定义可以另外具有定义签名的类型参数。:
julia> same_type(x::T, y::T) where {T} = true
same_type (generic function with 1 method)
julia> same_type(x,y) = false
same_type (generic function with 2 methods)
当两个参数具有相同的特定类型时,无论它是哪种类型,都使用第一种方法,而第二种方法作为通用方法,扩展到所有其他情况。 所以一般来说,这定义了一个布尔函数,检查它的两个参数是否具有相同的类型。:
julia> same_type(1, 2)
true
julia> same_type(1, 2.0)
false
julia> same_type(1.0, 2.0)
true
julia> same_type("foo", 2.0)
false
julia> same_type("foo", "bar")
true
julia> same_type(Int32(1), Int64(2))
false
此类定义对应于类型签名为"unionAll"类型的方法(请参阅部分 unionAll类型)。
通过调度对函数行为的这种定义是很常见的-惯用的-即使在Julia中也是如此。 方法类型参数不仅可以用作参数类型:它们可以应用于函数签名或函数体中值的任何位置。 下面是一个示例,其中方法类型参数’T’用作参数类型'Vector的类型参数{T}方法签名中的`:
julia> function myappend(v::Vector{T}, x::T) where {T}
return [v..., x]
end
myappend (generic function with 1 method)
此示例中的类型参数`T’确保添加的元素’x`是向量’v’的现有元素类型的子类型。 在定义方法签名后,`where’关键字提供了这些约束的列表。 它也适用于单行定义,如上所示,并且应该出现_before_ 返回类型声明,如果存在,如下所示:
julia> (myappend(v::Vector{T}, x::T)::Vector) where {T} = [v..., x]
myappend (generic function with 1 method)
julia> myappend([1,2,3],4)
4-element Vector{Int64}:
1
2
3
4
julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Vector{Int64}, ::Float64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
@ Main none:1
Stacktrace:
[...]
julia> myappend([1.0,2.0,3.0],4.0)
4-element Vector{Float64}:
1.0
2.0
3.0
4.0
julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Vector{Float64}, ::Int64)
The function `myappend` exists, but no method is defined for this combination of argument types.
Closest candidates are:
myappend(::Vector{T}, !Matched::T) where T
@ Main none:1
Stacktrace:
[...]
如果要添加的元素的类型与要添加它的向量元素的类型不匹配,则会发生错误。 'MethodError'。 在下面的示例中,方法类型参数’T’用作返回值。:
julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)
julia> mytypeof(1)
Int64
朱莉娅>mytypeof(1.0)
漂浮64
正如您可以在类型声明中对类型参数施加子类型限制一样(请参阅部分 Parametric types),也可以对方法类型的参数施加限制。
julia> same_type_numeric(x::T, y::T) where {T<:Number} = true
same_type_numeric (generic function with 1 method)
julia> same_type_numeric(x::Number, y::Number) = false
same_type_numeric (generic function with 2 methods)
julia> same_type_numeric(1, 2)
true
julia> same_type_numeric(1, 2.0)
false
julia> same_type_numeric(1.0, 2.0)
true
julia> same_type_numeric("foo", 2.0)
ERROR: MethodError: no method matching same_type_numeric(::String, ::Float64)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
Closest candidates are:
same_type_numeric(!Matched::T, ::T) where T<:Number
@ Main none:1
same_type_numeric(!Matched::Number, ::Number)
@ Main none:1
Stacktrace:
[...]
julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)
The function `same_type_numeric` exists, but no method is defined for this combination of argument types.
julia> same_type_numeric(Int32(1), Int64(2))
false
'Same_type_numeric’函数的工作方式与上面定义的`same_type`函数相同,但它仅针对数字对定义。
参数化方法允许使用与用于编写类型的`where`表达式相同的语法(请参阅部分 unionAll类型)。 如果只有一个参数,花括号(在'where {T})可以省略,但为了清楚起见,它们通常仍然是首选选项。 几个参数可以用逗号分隔,例如,'where {T, S<:Real}
,或者使用嵌套的`where’编写,例如’where S<:Real where T'。
重新定义方法
在重新定义方法或添加新方法时,重要的是要了解这些更改不会立即生效。 这是Julia能够静态输出和编译代码以快速执行的关键因素,而无需与JIT相关的通常技术和成本。 每个新方法定义对当前运行时环境都不可见,包括任务和线程(以及任何先前定义的"@generated"函数)。 让我们从一个例子开始,了解这意味着什么。
julia> function tryeval()
@eval newfun() = 1
newfun()
end
tryeval (generic function with 1 method)
julia> tryeval()
ERROR: MethodError: no method matching newfun()
The applicable method may be too new: running in world age xxxx1, while current world is xxxx2.
Closest candidates are:
newfun() at none:1 (method too new to be called from this world context.)
in tryeval() at none:1
...
julia> newfun()
1
请注意,在这个例子中,已经为`newfun`创建了一个新的定义,但不能立即调用它。 新的全局对象对`tryeval`函数立即可见,因此您可以编写’return newfun`(不带括号)。 但是你,也不是调用对象,也不是他们调用的函数都不能调用这个新的方法定义。
但是,有一个例外:将来对`newfun`_from REPL_的调用按预期工作,并且可以查看和调用`newfun`方法的新定义。
但是对`tryeval`的未来调用将继续看到’newfun’的定义,因为它在前一次调用REPL_之前是_,因此在这个调用`tryeval`之前。
你可以自己尝试一下,看看它是如何工作的。
此行为的实现是"世界年龄计数器"(方法定义的层次结构)。 这种单调增加的值跟踪每个方法定义操作。 "对给定运行时环境可见的一组方法定义"可以描述为单个数字,或"世界年龄"。 此外,可以通过简单地比较它们的序列号来比较两个世界中可用的方法。 上面的示例显示"当前世界"(其中存在`newfun`方法)比任务特定的"运行时世界"多一个,该世界在执行’tryeval’开始时被修复。
有时需要规避这种情况(例如,在实现上面描述的REPL的情况下)。 幸运的是,有一个简单的解决方案:使用以下方法调用函数 '基地。invokelatest':
julia> function tryeval2()
@eval newfun2() = 2
Base.invokelatest(newfun2)
end
tryeval2 (generic function with 1 method)
julia> tryeval2()
2
最后,让我们看一下这个规则开始适用的一些更复杂的例子。 定义函数’f(x’,它最初有一个方法:
julia> f(x) = "original definition"
f (generic function with 1 method)
使用`f(x)`运行其他操作:
julia> g(x) = f(x)
g (generic function with 1 method)
julia>t=@async f(wait());yield();
在`f(x)`中添加一些新方法:
julia> f(x::Int) = "definition for Int"
f (generic function with 2 methods)
julia> f(x::Type{Int}) = "definition for Type{Int}"
f (generic function with 3 methods)
比较这些结果之间的差异:
julia> f(1)
"definition for Int"
julia> g(1)
"definition for Int"
julia> fetch(schedule(t, 1))
"original definition"
julia> t = @async f(wait()); yield();
julia> fetch(schedule(t, 1))
"definition for Int"
使用参数化方法设计模式
虽然复杂的调度逻辑不是提高性能或可用性所必需的,但它有时可能是表达某些算法的最佳方式。 下面是一些常见的设计模式,有时会出现在这个调度用例中。
从超类型中提取类型参数
这是返回`AbstractArray`的任何任意子类型的元素类型`T`的正确代码模板,该子类型具有定义良好的元素类型。:
abstract type AbstractArray{T, N} end
eltype(::Type{<:AbstractArray{T}}) where {T} = T
采用所谓的三方调度。 请注意,类型是’unionAll',例如'eltype(AbstractArray{T} 其中T<:Integer)`,不匹配上述方法。 对于这种情况,'Base’中的`eltype’的实现为`Any`添加了备份方法。
其中一个常见的错误是试图通过内省来获取元素类型:
eltype_wrong(::Type{A}) where {A<:AbstractArray} = A.parameters[1]
但是,模拟失败的情况并不困难。:
struct BitVector <: AbstractArray{Bool, 1}; end
这里已经创建了`BitVector’类型,它没有参数,但元素类型仍然是完全定义的,并且`T`具有值`Bool'。
另一个错误是试图使用`supertype`提升类型层次结构:
eltype_wrong(::Type{AbstractArray{T}}) where {T} = T
eltype_wrong(::Type{AbstractArray{T, N}}) where {T, N} = T
eltype_wrong(::Type{A}) where {A<:AbstractArray} = eltype_wrong(supertype(A))
虽然这种方法适用于声明的类型,但对于没有超类型的类型则失败。:
julia> eltype_wrong(Union{AbstractArray{Int}, AbstractArray{Float64}})
ERROR: MethodError: no method matching supertype(::Type{Union{AbstractArray{Float64,N} where N, AbstractArray{Int64,N} where N}})
Closest candidates are:
supertype(::DataType) at operators.jl:43
supertype(::UnionAll) at operators.jl:48
使用不同类型的参数创建类似类型
在创建通用代码时,往往需要构建一个类似的对象,对类型结构进行一些修改,这也导致需要更改类型参数。 例如,您可能有一个包含任意类型元素的抽象数组,并且您希望编写将在其中执行的计算,其中包含特定类型的元素。 对于`AbstractArray的每个子类型{T}'有必要实现一个描述如何计算这种类型转换的方法。 没有从一个子类型到具有不同参数的另一个子类型的一般转换。
为了获得所需的结果,`AbstractArray`的子类型通常实现两种方法:一种将输入数组转换为特定抽象类型`AbstractArray’的子类型的方法。{T, N}`以及用于创建具有特定元素类型的新未初始化数组的方法。 其实现的例子可以在Julia Base中找到。 以下是它们使用的基本示例,确保’input’和`output’具有相同的类型。
input = convert(AbstractArray{Eltype}, input)
output = similar(input, Eltype)
此外,在算法需要输入数组副本的情况下,使用函数 'convert'将是不够的,因为返回值可能会替换原始输入。 组合 'similar'(创建输出数组)和 '复制!`(用输入数据填充它)是表达对输入参数的可变副本的要求的常用方式。:
copy_with_eltype(input, Eltype) = copyto!(similar(input, Eltype), input)
可迭代调度
对于调度多级参数参数列表,通常建议将每个调度级别划分为单独的函数。 这种方法似乎类似于单次调度,但是,正如稍后将看到的,它仍然更灵活。
例如,当尝试分派数组元素类型时,经常会出现不明确的情况。 但通常代码首先执行容器类型的分派,然后进行到更具体的基于eltype的方法。 在大多数情况下,算法很容易遵循这种分层方法,而在其他情况下,过程严格性问题必须手动解决。 例如,在对两个矩阵求和的逻辑中,可以观察到调度的这种分支。:
# Сначала диспетчеризация выбирает алгоритм сопоставления для поэлементного суммирования.
+(a::Matrix, b::Matrix) = map(+, a, b)
# Затем диспетчеризация обрабатывает каждый элемент и выбирает подходящий
# общий тип элемента для вычисления.
+(a, b) = +(promote(a, b)...)
# После получения элементов одинакового типа их можно сложить.
# Например, через примитивные операции, доступные в процессоре.
+(a::Float64, b::Float64) = Core.add(a, b)
基于特征的调度
上面描述的可迭代分派的一个自然扩展是向方法选择中添加一个层,该层允许您分派独立于类型层次结构定义的集合的类型集合。 这样一个集合可以通过编写感兴趣的"联合"类型来创建,但是这个集合是不可扩展的,因为"联合"类型在创建后不能更改。 然而,这样的可扩展集可以使用设计模式编程,通常称为https://github.com/JuliaLang/julia/issues/2345#issuecomment-54537633 [神圣特性]。
此模式通过定义一个通用函数来实现,该函数为函数参数可能属于的每组属性计算不同的单个值(或类型)。 如果此功能是干净的(没有副作用),则与常规调度相比,它不会影响性能。
在上一节中给出的示例中,省略了函数实现的细节。 '地图'和 '促进`,每个都根据这些标准工作。 当迭代矩阵时,例如在"map"的实现中,重要的问题之一是通过数据的顺序。 当’AbstractArray’的子类型实现一个特性时 '基地。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’函数,表达算法核心执行的一组操作。
-
计算结果矩阵的元素类型’R’为'promote_op(op,argument_types...)`,其中’argument_types`从应用于每个输入数组的`eltype’计算。
-
以"相似(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`
## но это не является истинным для некоторых типов, таких как Bool:
# 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(a, 1)
## здесь не используется `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
参数有限的方法,参数数量可变
函数参数也可以用来限制可以提供给具有可变参数数量(varargs)的函数的参数数量(具有可变参数数量的函数)。 符号’Vararg’用于指定此类内容。{T,N}`. 例如:
julia> bar(a,b,x::Vararg{Any,2}) = (a,b,x)
bar (generic function with 1 method)
julia> bar(1,2,3)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, !Matched::Any)
@ Main none:1
Stacktrace:
[...]
julia> bar(1,2,3,4)
(1, 2, (3, 4))
julia> bar(1,2,3,4,5)
ERROR: MethodError: no method matching bar(::Int64, ::Int64, ::Int64, ::Int64, ::Int64)
The function `bar` exists, but no method is defined for this combination of argument types.
Closest candidates are:
bar(::Any, ::Any, ::Any, ::Any)
@ Main none:1
Stacktrace:
[...]
将参数数量可变的方法限制为参数更有用。 例如:
function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}
只有当`索引`的数量对应于数组的维度时,才会调用它。
当需要仅限制提供的参数类型时,'Vararg{T}也可以写成'T。..
. 例如,+f(x::Int。..)=x+'是
+f(x::Vararg的缩写{Int})=x+`。
关于可选参数和命名参数的说明
章中简要提到的 函数,可选参数被实现为多个方法定义的语法。 例如,这个定义:
f(a=1,b=2) = a+2b
它转换成以下三种方法:
f(a,b) = a+2b
f(a) = f(a,2)
f() = f(1,2)
这意味着调用’f()相当于调用’f(1,2)'。 在这种情况下,结果将是'5
,因为`f(1,2)`调用上面的第一个方法`f'。 然而,情况并非总是如此。 如果我们定义第四种方法,更专门用于整数:
f(a::Int,b::Int) = a-2b
f()`和`f(1,2)`的结果将是
-3`。 换句话说,可选参数绑定到函数,而不是该函数的任何特定方法。 被调用的方法取决于可选参数的类型。 如果根据全局变量定义可选参数,则可选参数的类型甚至可能在执行过程中更改。
命名参数的作用与常规位置参数完全不同。 特别是,它们不涉及调度方法。 仅基于位置参数调度方法,并在确定适当的方法后处理命名参数。
像函数这样的对象
方法与类型相关,因此任何任意Julia对象都可以通过向其类型添加方法来使其"可调用"。 (这样的"可调用"对象有时被称为"函子"。)
例如,您可以定义一种类型,该类型存储多项式的系数,但充当计算此多项式的函数。:
julia> struct Polynomial{R}
coeffs::Vector{R}
end
julia>函数(p::多项式)(x)
系数[结束]
对于i=(length(p.coeffs)-1):-1:1
v=v*x+p.系数[i]
结束
返回v
结束
julia>(p::多项式)()=p(5)
请注意,函数是按类型指定的,而不是按名称指定的。 与常规函数一样,有一个简短的语法。 在函数的主体中,'p’将引用被调用的对象。 '多项式’可以如下使用:
julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])
julia> p(3)
931
julia> p()
2551
该机制还决定了类型构造函数和闭包(引用其环境的内部函数)在Julia中的工作方式。
方法的发展和消除歧义
Julia中方法的多态性是这种语言最强大的功能之一,但是,它的使用可能会在开发过程中造成困难。 特别是,在更复杂的方法层次结构中,经常有 歧义。
上面指出,为了消除模棱两可,如
f(x, y::Int) = 1
f(x::Int, y) = 2
您可以定义一个方法
f(x::Int, y::Int) = 3
大多数情况下,这个决定将是正确的,但在某些情况下,不假思索地遵循这个建议可能会导致相反的结果。 特别是,通用函数的方法越多,模糊的可能性就越高。 当方法的层次结构变得比这个简单的例子更复杂时,可能需要仔细考虑替代策略。
接下来,将讨论具体问题和一些替代解决方案。
元组和NTuple参数
参数’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{}) = 3
或者,对于除一个之外的所有方法,必须满足元组中至少有一个元素的条件。:
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'。 这是一个更一般的原则的一个非常具体的例子。 https://en.wikipedia.org/wiki/Orthogonality_ (编程)[正交设计],其中单独的概念被分配给单独的方法。 在这里,`g’很可能需要一个备份定义。
g(x::A) = x
一个相关的策略使用"促进"将"x"和"y"带到一个共同的类型。:
f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)
这种情况的风险之一如下:如果没有合适的提升方法将`x`和`y`转换为一种类型,第二种方法将无限重复,并会导致堆栈溢出。
一次调度一个参数
如果您需要分派多个参数,并且有太多组合的备份方法来确定所有可用选项,请考虑在(例如)您分派第一个参数然后调用内部方法时引入"名称级联"。:
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`和
-`)来实现它。 这只是一个更复杂的建议版本。 正交化方法。
当这种方法是不可能的,它可能是值得组织讨论与其他开发人员关于歧义的解决。 首先定义一个方法的事实并不一定意味着不能更改或取消使用它。 作为最后的手段,其中一个开发人员可以确定临时解决方案。,
-(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())#默认限制条件
函数myfilter(a,内核,::复制)
Apadded=replicate_edges(A,大小(内核))
myfilter(Apadded,kernel,NoPad())#指定新的限制条件
结束
#以下是其他填充方法
函数myfilter(a,内核,::NoPad)
#这是"真正的"实现
结束内核计算
'NoPad’被指定在与任何其他类型的填充相同的参数位置,因此调度层次结构保持良好的组织,不太可能出现歧义。 此外,它扩展了’myfilter`的"公共"接口:想要显式控制填充的用户可以直接调用’NoPad`选项。
在局部区域中定义方法
您可以在 局部区域,例如:
julia> function f(x)
g(y::Int) = y + x
g(y) = y - x
g
end
f (generic function with 1 method)
julia> h = f(3);
julia> h(4)
7
julia> h(4.0)
1.0
但是,不应有条件地或根据执行顺序定义本地方法,例如:
function f2(inc)
if inc
g(x) = x + 1
else
g(x) = x - 1
end
end
function f3()
function g end
return g
g() = 0
end
因为它将不清楚哪个函数最终将被定义。 将来,局部方法的这种定义可能会被认为是错误的。
在这种情况下,请使用匿名函数。:
function f2(inc)
g = if inc
x -> x + 1
else
x -> x - 1
end
end