AnyMath 文档

方法

召回自 函数函数是将参数元组映射到返回值的对象,或者如果不能返回适当的值,则抛出异常。 对于不同类型的参数,相同的概念函数或操作的实现方式有很大不同:添加两个整数与添加两个浮点数非常不同,这两个浮点数都不同于将整数添加到浮点数。 尽管它们的实施差异,但这些操作都属于"加法"的一般概念。 因此,在Julia中,这些行为都属于一个对象: + 函数。

为了便于顺利使用同一概念的许多不同实现,函数不需要一次全部定义,而是可以通过为参数类型和计数的某些组合提供特定行为来分段定义。 函数的一种可能行为的定义称为_method_。 到目前为止,我们只提供了使用单个方法定义的函数的示例,适用于所有类型的参数。 但是,方法定义的签名可以被注释以指示除其数量之外的参数类型,并且可以提供多个方法定义。 当函数应用于特定的参数元组时,应用适用于这些参数的最具体的方法。 因此,函数的整体行为是其各种方法定义的行为的拼凑。 如果拼凑设计得很好,即使方法的实现可能有很大不同,函数的外部行为也会显得无缝且一致。

应用函数时要执行的方法的选择称为_dispatch_。 Julia允许调度过程根据给定的参数数量和所有函数参数的类型来选择调用函数的方法。 这与传统的面向对象语言不同,传统的面向对象语言仅基于第一个参数进行调度,第一个参数通常具有特殊的参数语法,并且有时是隐含的而不是显式 脚注:1[在C中++ 或者Java,例如,在像这样的方法调用中 反对。甲基苯丙胺(arg1,arg2),对象obj"接收"方法调用,并通过隐式传递给方法 关键字,而不是作为显式方法参数。 当电流 对象是方法调用的接收者,它可以完全省略,只写 甲基苯丙胺(arg1,arg2),与 暗示为接收对象。]使用函数的所有参数来选择应该调用哪个方法,而不仅仅是第一个方法,被称为https://en.wikipedia.org/wiki/Multiple_dispatch[多重派遣]。 多重调度对于数学代码特别有用,在这种情况下,人为地认为操作比其他任何一个参数更"属于"一个参数是没有意义的:加法操作是否在 x+y 属于 x 比这更重要 y? 数学运算符的实现通常取决于其所有参数的类型。 然而,除了数学运算之外,多重调度最终成为构建和组织程序的强大而方便的范例。

注意本章中的所有示例都假定您正在为_same_模块中的函数定义方法。 如果要在_another_模块中向函数添加方法,则必须 进口 它或使用模块名称限定的名称。 请参阅 命名空间管理

定义方法

到目前为止,在我们的示例中,我们只定义了具有无约束参数类型的单个方法的函数。 这些函数的行为就像在传统的动态类型语言中一样。 尽管如此,我们几乎不断地使用多个调度和方法,而没有意识到它:所有Julia的标准函数和运算符,就像前面提到的那样 + 函数,有许多方法通过参数类型和计数的各种可能组合来定义它们的行为。

在定义函数时,可以选择使用 :: 类型断言运算符,在 复合类型:

julia> f(x::Float64, y::Float64) = 2x + y
f (generic function with 1 method)

此函数定义仅适用于以下位置的调用 xy 两个值都是type 漂浮64:

julia> f(2.0, 3.0)
7.0

将其应用于任何其他类型的参数将导致 方法;方法:

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

Stacktrace:
[...]

julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
The function `f` exists, but no method is defined for this combination of argument types.

最接近的候选人是:
  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` 存在,但没有为这种参数类型的组合定义任何方法。

正如你所看到的,参数必须是精确的类型 漂浮64. 其他数字类型(如整数或32位浮点值)不会自动转换为64位浮点,也不会将字符串解析为数字。 因为 漂浮64 是一个具体类型,具体类型不能在Julia中被子类化,这样的定义只能应用于类型完全相同的参数 漂浮64. 但是,编写声明的参数类型是抽象的更一般的方法可能会很有用:

julia> f(x::Number, y::Number) = 2x - y
f (generic function with 2 methods)

julia> f(2.0, 3)
1.0

此方法定义适用于作为 电话号码. 它们不必是相同类型的,只要它们是每个数值即可。 处理不同数字类型的问题委托给表达式中的算术运算 2x-y.

要使用多个方法定义函数,只需使用不同数量和类型的参数多次定义函数。 函数的第一个方法定义创建函数对象,随后的方法定义将新方法添加到现有函数对象。 当应用函数时,将执行与参数的数量和类型匹配的最具体的方法定义。 因此,上面的两个方法定义结合在一起,定义了 f 在抽象类型的所有实例对上 电话号码 --但有不同的行为特定于对 漂浮64值。 如果其中一个参数是64位浮点数,但另一个不是,那么 f(Float64,Float64) 方法不能被调用,更一般 f(数字,数字) 必须使用方法:

julia> f(2.0, 3.0)
7.0

julia> f(2, 3.0)
1.0

julia> f(2.0, 3)
1.0

julia> f(2, 3)
1

2x+y 定义仅用于第一种情况,而 2x-y 定义在其他中使用。 不执行函数参数的自动转换或转换:Julia中的所有转换都是非神奇的,完全显式的。 转换和推广,但是,显示了如何巧妙地应用足够先进的技术可以从魔术无法区分。 脚注:Clarke61[Arthur C.Clarke,Profiles Of The Future(1961):Clarke’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)
# 2 methods for generic function "f" from Main:
 [1] f(x::Float64, y::Float64)
     @ none:1
 [2] f(x::Number, y::Number)
     @ none:1

这表明 f 有两种方法,一个取两个 漂浮64 参数和一个类型参数 电话号码. 它还指示定义方法的文件和行号:因为这些方法是在REPL定义的,所以我们得到了明显的行号 无:1.

在没有类型声明与 ::,方法参数的类型为 任何 默认情况下,这意味着它是无约束的,因为Julia中的所有值都是抽象类型的实例 任何. 因此,我们可以为 f 像这样:

julia> f(x,y) = println("Whoa there, Nelly.")
f (generic function with 3 methods)

julia> methods(f)
# 3 methods for generic function "f" from Main:
 [1] f(x::Float64, y::Float64)
     @ none:1
 [2] f(x::Number, y::Number)
     @ none:1
 [3] f(x, y)
     @ none:1

julia> f("foo", 1)
Whoa there, Nelly.

对于一对参数值,这个catch-all比任何其他可能的方法定义都不那么具体,因此它只会在没有其他方法定义适用的参数对上调用。

请注意,在第三种方法的签名中,没有为参数指定类型 xy. 这是一种缩短的表达方式 f(x::Any,y::Any).

虽然这似乎是一个简单的概念,但对值类型的多重调度可能是Julia语言中最强大和最核心的特征。 核心操作通常有几十种方法:

julia> methods(+)
# 180 methods for generic function "+":
[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能够抽象地表达与实现细节分离的高级算法。

方法专业化

当您创建同一函数的多个方法时,这有时称为"特化"。"在这种情况下,您通过向_function_添加其他方法来专门化它:每个新方法都是函数的新特化。 如上所示,这些特化由 方法.

还有另一种专门化是在没有程序员干预的情况下发生的:Julia的编译器可以自动为使用的特定参数类型专门化_method_。 这样的专业是由 方法,因为这不会创造新的 方法s,但工具像 @code_typed允许您检查此类专业。

例如,如果创建一个方法

mysum(x::Real, y::Real) = x + y

你已经给出了这个函数 我自己 一个新的方法(可能是它唯一的方法),并且该方法采用任何一对 真实的 号输入。 但如果你执行

julia> mysum(1, 2)
3

julia> mysum(1.0, 2.0)
3.0

Julia将编译 我自己 两次,一次 x::Int,y::Int 再次为 x::Float64,y::Float64. 两次编译的重点是性能:被调用的方法 + (哪个 我自己 用途)因具体类型而异 xy,并通过编译不同的专业化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) 方法。 定义方法的顺序无关紧要,也没有比其他方法更具体。 在这种情况下,朱莉娅提出了一个 方法;方法而不是任意挑选一种方法。 您可以通过为交集情况指定适当的方法来避免方法歧义:

julia> g(x::Float64, y::Float64) = 2x + 2y
g (generic function with 3 methods)

julia> g(2.0, 3)
7.0

julia> g(2, 3.0)
8.0

julia> 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类型)。

这种通过调度来定义函数行为的定义在Julia中是很常见的—​惯用的,甚至--。 方法类型参数不限于用作参数类型:它们可以在函数或函数体签名中的任何值中使用。 下面是一个方法类型参数的示例 T 用作参数类型的类型参数 向量{T} 在方法签名:

julia> function myappend(v::Vector{T}, x::T) where {T}
           return [v..., x]
       end
myappend (generic function with 1 method)

类型参数 T 在此示例中确保添加的元素 x 是向量的现有eltype的子类型 v. 该 哪里 关键字在方法签名定义之后引入这些约束的列表。 这对于单行定义的工作原理相同,如上所述,并且必须出现_before_the 返回类型声明,如果存在,如下所示:

julia> (myappend(v::Vector{T}, x::T)::Vector) where {T} = [v..., x]
myappend (generic function with 1 method)

朱莉娅>myappend([1,2,3],4)
4元素向量{Int64}:
 1
 2
 3
 4

朱莉娅>myappend([1,2,3],2.5)
错误:MethodError:没有匹配myappend(::Vector的方法{Int64},::Float64)
的功能 `我的申请` 存在,但没有为这种参数类型的组合定义任何方法。

最接近的候选人是:
  myappend(::向量{T}, !匹配::T)其中T
   @主要无:1

[医]堆垛机:
[...]

朱莉娅>myappend([1.0,2.0,3.0],4.0)
4元素向量{Float64}:
 1.0
 2.0
 3.0
 4.0

朱莉娅>myappend([1.0,2.0,3.0],4)
错误:MethodError:没有匹配myappend(::Vector的方法{Float64},::Int64)
的功能 `我的申请` 存在,但没有为此参数类型组合定义任何方法。

最接近的候选人是:
  myappend(::向量{T}, !匹配::T)其中T
   @主要无:1

[医]堆垛机:
[...]

如果附加元素的类型与其附加到的向量的元素类型不匹配,则 方法;方法被提出。 在下面的示例中,方法的类型参数 T 用作返回值:

julia> mytypeof(x::T) where {T} = T
mytypeof (generic function with 1 method)

julia> mytypeof(1)
Int64

julia> mytypeof(1.0)
Float64

就像您可以在类型声明中对类型参数放置子类型约束一样(请参阅 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)
错误

julia>same_type_numeric(1.0,2.0)
真的

julia>same_type_numeric("foo",2.0)
错误:MethodError:没有匹配same_type_numeric(::String,::Float64)的方法
的功能 `同_type_numeric` 存在,但没有为这种参数类型的组合定义任何方法。

最接近的候选人是:
  same_type_numeric(!匹配::T,::T)其中T<:Number
   @主要无:1
  same_type_numeric(!匹配::号码,::号码)
   @主要无:1

[医]堆垛机:
[...]

julia>same_type_numeric("foo","bar")
错误:MethodError:没有匹配same_type_numeric(::String,::String)的方法
的功能 `同_type_numeric` 存在,但没有为这种参数类型的组合定义任何方法。

julia>same_type_numeric(Int32(1),Int64(2))
错误

同_type_numeric 函数的行为很像 same_type 上面定义的函数,但仅针对数字对定义。

参数化方法允许使用与 哪里 用于写入类型的表达式(请参阅 UnionAll类型)。 如果只有一个参数,则包围花括号(在 在哪里 {T})可以省略,但为了清楚起见通常是优选的。 多个参数可以用逗号分隔,例如 哪里 {T, S<:Real},或使用嵌套编写 哪里,例如 where S<:Real where T.

重新定义方法

在重新定义方法或添加新方法时,重要的是要意识到这些更改不会立即生效。 这是Julia能够静态推断和编译代码以快速运行的关键,而无需通常的JIT技巧和开销。 实际上,任何新的方法定义对当前运行时环境都不可见,包括任务和线程(以及任何先前定义的 @生成 功能)。 让我们从一个例子开始,看看这意味着什么:

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

在这个例子中,观察到新的定义为 纽芬兰 已创建,但不能立即调用。 新的全局对 [医]特里瓦尔 功能,所以你可以写 返回纽芬兰 (不带括号)。 但既不是你,也不是你的任何呼叫者,也不是他们调用的功能,等等。 可以调用这个新的方法定义!

但有一个例外:未来调用 纽芬兰 _从REPL_按预期工作,能够看到和调用新的定义 纽芬兰.

然而,未来的呼吁 [医]特里瓦尔 将继续看到的定义 纽芬兰 因为它是在REPL_上一条语句,因此在调用 [医]特里瓦尔.

你可能想自己试试这个,看看它是如何工作的。

这种行为的实施是一个"世界年龄计数器",在 Worldage手册章节。

使用参数化方法设计模式

虽然性能或可用性不需要复杂的调度逻辑,但有时它可能是表达某些算法的最佳方式。 以下是在以这种方式使用调度时有时会出现的一些常见设计模式。

从超级类型中提取类型参数

下面是返回元素类型的正确代码模板 T 的任何任意子类型的 抽象阵列 它具有明确定义的元素类型:

abstract type AbstractArray{T, N} end
eltype(::Type{<:AbstractArray{T}}) where {T} = T

使用所谓的三角调度。 请注意 联合所有 类型,例如 eltype(AbstractArray{T} 其中T<:整数),与上述方法不匹配。 的实施 eltype,eltype基地 将回退方法添加到 任何 对于这种情况。

一个常见的错误是尝试使用内省来获取元素类型:

eltype_wrong(::Type{A}) where {A<:AbstractArray} = A.parameters[1]

但是,构建这将失败的案例并不难:

struct BitVector <: AbstractArray{Bool, 1}; end

在这里,我们创建了一个类型 比特克托 它没有参数,但元素类型仍然是完全指定的, T 等于 布尔!

另一个错误是尝试使用 超类型:

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{Vector{Int}, Matrix{Int}})
ERROR: MethodError: no method matching supertype(::Type{VecOrMat{Int64}})

Closest candidates are:
  supertype(::UnionAll)
   @ Base operators.jl:44
  supertype(::DataType)
   @ Base operators.jl:43

使用不同的类型参数构建类似的类型

在构建泛型代码时,通常需要构造一个类似的对象,并对类型的布局进行一些更改,这也需要更改类型参数。 例如,您可能有某种具有任意元素类型的抽象数组,并希望使用特定元素类型在其上编写计算。 我们必须为每个实现一个方法 抽象阵列{T} 描述如何计算此类型转换的子类型。 没有将一个子类型转换为具有不同参数的另一个子类型的一般转换。

的亚型 抽象阵列 通常实现两种方法来实现这一点:将输入数组转换为特定的子类型的方法 抽象阵列{T, N} 抽象类型;以及制作具有特定元素类型的新未初始化数组的方法。 这些示例实现可以在Julia Base中找到。 以下是它们的基本示例用法,保证 输入输出 是同一类型的:

input = convert(AbstractArray{Eltype}, input)
output = similar(input, Eltype)

作为对此的扩展,在算法需要输入数组副本的情况下, 转换/转换是不够的,因为返回值可能会别名原始输入。 合并;合并 类似的(使输出数组)和 收到!(用输入数据填充它)是一种通用的方式来表达对输入参数的可变副本的要求:

copy_with_eltype(input, Eltype) = copyto!(similar(input, Eltype), input)

迭代调度

为了调度一个多级参数参数列表,通常最好将每个级别的调度分成不同的函数。 这可能听起来类似于单一调度,但正如我们将在下面看到的,它仍然更灵活。

例如,尝试对数组的元素类型进行调度通常会遇到不明确的情况。 相反,通用代码将首先在容器类型上进行调度,然后递归到基于 eltype,eltype. 在大多数情况下,算法可以方便地使用这种分层方法,而在其他情况下,这种严格性必须手动解决。 例如,在对两个矩阵求和的逻辑中,可以观察到这种调度分支:

# First dispatch selects the map algorithm for element-wise summation.
+(a::Matrix, b::Matrix) = map(+, a, b)
# Then dispatch handles each element and selects the appropriate
# common element type for the computation.
+(a, b) = +(promote(a, b)...)
# Once the elements have the same type, they can be added.
# For example, via primitive operations exposed by the processor.
+(a::Float64, b::Float64) = Core.add(a, b)

基于特征的调度

上面迭代调度的一个自然扩展是在方法选择中添加一个层,该层允许在独立于类型层次结构定义的集合的类型集合上进行调度。 我们可以通过写出一个 工会 有问题的类型,但是这个集合不能扩展为 工会-创建后无法更改类型。 然而,这样的可扩展集可以用通常称为a的设计模式编程https://github.com/JuliaLang/julia/issues/2345#issuecomment-54537633["神圣特质"]。

这种模式是通过定义一个泛型函数来实现的,该函数为函数参数可能属于的每个特性集计算不同的单例值(或类型)。 如果此函数是纯的,则与正常调度相比,对性能没有影响。

上一节中的示例掩盖了 地图推广,它们都在这些性状方面起作用。 当迭代一个矩阵,如在实现 地图,一个重要的问题是使用什么顺序来遍历数据。 何时 抽象阵列 子类型实现 基地。索引样式特质,其他功能如 地图 可以根据此信息调度以选择最佳算法(参见 抽象数组接口)。 这意味着每个子类型不需要实现自定义版本的 地图,因为泛型定义+特质类将使系统能够选择最快的版本。 这里是一个玩具实现 地图 说明基于特征的调度:

map(f, a::AbstractArray, b::AbstractArray) = map(Base.IndexStyle(a, b), f, a, b)
# generic implementation:
map(::Base.IndexCartesian, f, a::AbstractArray, b::AbstractArray) = ...
# linear-indexing implementation (faster)
map(::Base.IndexLinear, f, a::AbstractArray, b::AbstractArray) = ...

这种基于特征的方法也存在于 推广标量采用的机制 +. 它使用 promote_type,它返回给定两种类型的操作数计算操作的最佳公共类型。 这使得可以减少为每对可能的类型参数实现每个函数的问题,到实现从每个类型到公共类型的转换操作的小得多的问题,再加上一个首选的成对提升规则表。

输出型计算

基于特征的提升的讨论提供了一个过渡到我们的下一个设计模式:计算矩阵操作的输出元素类型。

对于实现原始操作,例如加法,我们使用 promote_type函数来计算所需的输出类型。 (和以前一样,我们在工作中看到了这一点 推广 打电话给 +).

对于矩阵上更复杂的函数,可能需要计算更复杂的操作序列的预期返回类型。 这通常通过以下步骤执行:

  1. 写一个小函数 op的 表示由算法的内核执行的操作集。

  2. 计算元素类型 R 结果矩阵为 promote_op(op,argument_types。..),在哪里 论证类型 是从 eltype,eltype 应用于每个输入阵列。

  3. 将输出矩阵构建为 类似(R,dims),在哪里 暗淡无光 是输出阵列的所需尺寸。

对于一个更具体的例子,一个通用的方阵乘伪代码可能看起来像:

function matmul(a::AbstractMatrix, b::AbstractMatrix)
    op = (ai, bi) -> ai &ast; bi + ai &ast; bi

    ## this is insufficient because it assumes `one(eltype(a))` is constructable:
    # R = typeof(op(one(eltype(a)), one(eltype(b))))

    ## this fails because it assumes `a[1]` exists and is representative of all elements of the array
    # R = typeof(op(a[1], b[1]))

    ## this is incorrect because it assumes that `+` calls `promote_type`
    ## but this is not true for some types, such as Bool:
    # R = promote_type(ai, bi)

    # this is wrong, since depending on the return value
    # of type-inference is very brittle (as well as not being optimizable):
    # R = Base.return_types(op, (eltype(a), eltype(b)))

    ## but, finally, this works:
    R = promote_op(op, eltype(a), eltype(b))
    ## although sometimes it may give a larger type than desired
    ## it will always give a correct type

    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)
                ## here we don't use `ab = zero(R)`,
                ## since `R` might be `Any` and `zero(Any)` is not defined
                ## we also must declare `ab::R` to make the type of `ab` constant in the loop,
                ## since it is possible that typeof(a &ast; b) != typeof(a &ast; b + a &ast; b) == R
                ab::R = a[i, 1] &ast; b[1, j]
                for k in 2:size(a, 2)
                    ab += a[i, k] &ast; 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)...)

参数约束的Varargs方法

函数参数也可以用来约束可能提供给"varargs"函数的参数数量(Varargs函数)。 符号 瓦拉格{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:
[...]

更有用的是,可以通过参数来约束varargs方法。 例如:

function getindex(A::AbstractArray{T,N}, indices::Vararg{Number,N}) where {T,N}

只有当 指数 匹配数组的维数。

当只需要约束提供的参数的类型时 瓦拉格{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&ast;x+p.系数[i]
           结束
           返回v
       结束

julia>(p::多项式)()=p(5)

请注意,函数是按类型而不是按名称指定的。 与普通函数一样,有一个简洁的语法形式。 在功能体, p 将引用被调用的对象。 A 多项式 可以如下使用:

julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])

julia> p(3)
931

julia> p()
2551

这种机制也是类型构造函数和闭包(引用其周围环境的内部函数)如何在Julia中工作的关键。

空泛型函数

有时,在不添加方法的情况下引入泛型函数是很有用的。 这可用于将接口定义与实现分开。 它也可能是为了文档或代码可读性而完成的。 此语法为空 功能 没有参数元组的块:

function emptyfunc end

方法设计和避免歧义

Julia的方法多态性是其最强大的功能之一,但利用这种功能可能会带来设计挑战。 特别是,在更复杂的方法层次结构中,对于 含糊不清出现。

上面指出,人们可以解决模棱两可的问题,如

f(x, y::Int) = 1
f(x::Int, y) = 2

通过定义方法

f(x::Int, y::Int) = 3

这通常是正确的策略;然而,在某些情况下,盲目地遵循这个建议可能会适得其反。 特别是,泛型函数的方法越多,歧义的可能性就越大。 当你的方法层次结构变得比这个简单的例子更复杂时,仔细考虑替代策略是值得的。

下面我们讨论特定的挑战和解决这些问题的一些替代方法。

元组和NTuple参数

元组 (和 N.婴儿 争论带来了特殊的挑战。 例如,

f(x::NTuple{N,Int}) where {N} = 1
f(x::NTuple{N,Float64}) where {N} = 2

是模棱两可的,因为 N==0:没有元素来确定是否 Int型漂浮64 应调用变体。 为了解决歧义,一种方法是为空元组定义一个方法:

f(x::Tuple{}) = 3

或者,对于除了一个之外的所有方法,您可以坚持元组中至少有一个元素:

f(x::NTuple{N,Int}) where {N} = 1           # this is the fallback
f(x::Tuple{Float64, Vararg{Float64}}) = 2   # this requires at least one 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

相关策略利用 推广 带来 xy 到通用类型:

f(x::T, y::T) where {T} = ...
f(x, y) = f(promote(x, y)...)

这种设计的一个风险是,如果没有合适的推广方法,转换的可能性 xy 对于相同的类型,第二个方法将无限地递归并触发堆栈溢出。

一次发送一个参数

如果您需要在多个参数上进行调度,并且有许多具有太多组合的回退以使定义所有可能的变体变得实用,那么考虑引入"名称级联",其中(例如)您在第一个:

f(x::A, y) = _fA(x, y)
f(x::B, y) = _fB(x, y)

然后内部方法 _发_fB 可以在 y 不关心彼此之间在 x.

请注意,此策略至少有一个主要缺点:在许多情况下,用户无法进一步自定义 f 通过定义导出函数的进一步特化 f. 相反,他们必须为您的内部方法定义特化 _发_fB,这模糊了导出和内部方法之间的界限。

抽象容器和元素类型

在可能的情况下,尽量避免定义在抽象容器的特定元素类型上分派的方法。 例如,

-(A::AbstractArray{T}, b::Date) where {T<:Date}

为定义方法的任何人生成歧义

-(A::MyArrayType{T}, b::T) where {T}

最好的方法是避免定义这些方法中的_either_:相反,依赖泛型方法 -(A::抽象阵列,b) 并确保使用泛型调用实现此方法(如 类似的-)这对每个容器类型和元素类型_separately_做了正确的事情。 这只是建议的一个更复杂的变体 正交化你的方法。

当这种方法不可能时,可能值得与其他开发人员开始讨论解决歧义问题;仅仅因为首先定义了一种方法并不一定意味着它不能被修改或消除。 作为最后的手段,一个开发人员可以定义"创可贴"方法

-(A::MyArrayType{T}, b::Date) where {T<:Date} = ...

这通过蛮力解决了模棱两可的问题。

带有默认参数的复杂方法"级联"

如果您正在定义一个提供默认值的方法"cascade",请小心删除与潜在默认值相对应的任何参数。 例如,假设您正在编写一个数字滤波算法,并且您有一个通过应用填充来处理信号边缘的方法:

function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel)  # now perform the "real" computation
end

这将与提供默认填充的方法相抵触:

myfilter(A, kernel) = myfilter(A, kernel, Replicate()) # replicate the edge by default

这两种方法一起生成一个无限递归 A 不断变大。

更好的设计是像这样定义您的调用层次结构:

struct NoPad end  # indicate that no padding is desired, or that it's already applied

myfilter(A, kernel) = myfilter(A, kernel, Replicate())  # default boundary conditions

function myfilter(A, kernel, ::Replicate)
    Apadded = replicate_edges(A, size(kernel))
    myfilter(Apadded, kernel, NoPad())  # indicate the new boundary conditions
end

# other padding methods go here

function myfilter(A, kernel, ::NoPad)
    # Here's the "real" implementation of the core computation
end

NoPad的 被提供在与任何其他类型的填充相同的参数位置,因此它保持调度层次结构良好的组织和减少歧义的可能性。 此外,它扩展了"公共" 我的过滤器 接口:想要显式控制填充的用户可以调用 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

但是,您应该_not_有条件地定义本地方法或受控制流的约束,如

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