julia:方法(完结)

我们回想一下,在函数中我们知道函数是这么一个对象,它把一组参数映射成一个返回值,或者当没有办法返回恰当的值时扔出一个异常。对于相同概念的函数或者运算对不同的参数类型有十分不一样的实现这件事是普遍存在的:两个整数的加法与两个浮点数的加法是相当不一样的,整数与浮点数之间的加法也不一样。除开他们实现上的不同,这些运算都归在”加法”这么一个通用概念之下。因此在Julia中这些行为都属于一个对象:+函数。

为了让对同样的概念使用许多不同的实现这件事更顺畅,函数没有必要马上全部都被定义,反而应该是一块一块地定义,为特定的参数类型和数量的组合提供指定的行为。对于一个函数的一个可能行为的定义叫做方法。直到这里,我们只展示了那些只定了一个方法的,对参数的所有类型都适用的函数。但是方法定义的特点是不仅能表明参数的数量,也能表明参数的类型,并且能提供多个方法定义。当一个函数被应用于特殊的一组参数时,能用于这一组参数的最特定的方法会被使用。所以,函数的全体行为是他的不同的方法定义的行为的组合。如果这个组合被设计得好,即使方法们的实现之间会很不一样,函数的外部行为也会显得无缝而自洽。

当一个函数被应用时执行方法的选择被称为分派。Julia允许分派过程来基于给的参数的个数和所有的参数的类型来选择调用函数的哪个方法。这与传统的面对对象的语言不一样,面对对象语言的分派只基于第一参数,经常有特殊的参数语法并且有时是暗含而非显式写成一个参数。[1]使用函数的所有参数,而非只用第一个,来决定调用哪个方法被称为多重分派。多重分派对于数学代码来说特别有用,人工地将运算视为对于其中一个参数的属于程度比其他所有的参数都强的这个概念对于数学代码是几乎没有意义的:x + y中的加法运算对x的属于程度比对y更强?一个数学运算符的实现普遍基于它所有的参数的类型。即使跳出数学运算,多重分派是对于结构和组织程序来说也是一个强大而方便的范式。

例如,在c或java中,在像obj.meth(arg1,arg2)这样的方法调用中,Object obj“接收”方法调用,并通过this关键字隐式传递给方法,而不是作为显式方法argument。当前的该对象是方法调用的接收者时,可以完全省略它,只编写方法(arg1,arg2),这意味着接收对象。

定义方法


直到这里,在我们的例子中,我们只定义了只有一个不限制参数类型的方法的函数。这样的函数的行为就像在传统的动态类型的语言中一样。不过,我们已经在没有意识到的情况下使用了多重分派和方法:所有的Julia的标准函数和运算符,就像之前提到的+函数,都有基于参数类型和数量的不同可能的组合而定义的大量方法。

当定义一个函数时,可以视需要来约束可以应用的参数类型,使用在Composite Types中介绍的::类型断言运算符。

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

此函数定义仅适用于x和y都是Float 64类型值的调用:

julia> f(2.0, 3.0)
7.0

将其运用于其他任意的参数类型会导致 MethodError:

julia> f(2.0, 3)
ERROR: MethodError: no method matching f(::Float64, ::Int64)
Closest candidates are:
  f(::Float64, !Matched::Float64) at none:1

julia> f(Float32(2.0), 3.0)
ERROR: MethodError: no method matching f(::Float32, ::Float64)
Closest candidates are:
  f(!Matched::Float64, ::Float64) at none:1

julia> f(2.0, "3.0")
ERROR: MethodError: no method matching f(::Float64, ::String)
Closest candidates are:
  f(::Float64, !Matched::Float64) at none:1

julia> f("2.0", "3.0")
ERROR: MethodError: no method matching f(::String, ::String)

如同你所看到的,参数必须精确地是类型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

这个方法定义应用于任意一对Number的实例的参数。他们不需要是同一类型的,只要他们都是数字值。操作不同数字类型的问题就委派给了表达式2x - y中的算法运算。

为了定义一个有多个方法的函数,只需简单定义这个函数多次,使用不同的参数数量和类型。函数的第一个方法定义会建立这个函数对象,后续的方法定义会添加新的方法到存在的函数对象中去。当函数被应用时,最符合参数的数量和类型的特定方法会被执行。所以,上面的两个方法定义在一起定义了函数f对于所有的一对虚拟类型Number实例的行为 – 但是针对一对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

julia> f(2, 3)
1

2x + y定义只用于第一个情况,2x - y定义用于其他的情况。没有使用任何自动的函数参数的指派或者类型转换:Julia中的所有转换都不是magic的,都是完全显式的。然而类型转换和类型提升显示了足够先进的技术的智能应用能够与magic不可分辨到什么程度。[Clark61]对于非数字值,和比两个参数更多或者更少的情况,函数f并没有定义,应用会导致MethodError:

julia> f("foo", 3)
ERROR: MethodError: no method matching f(::String, ::Int64)
Closest candidates are:
 f(!Matched::Number, ::Number) at none:1

julia> f()
ERROR: MethodError: no method matching f()
Closest candidates are:
 f(!Matched::Float64, !Matched::Float64) at none:1
 f(!Matched::Number, !Matched::Number) at none:1

可以简单地看到对于函数存在哪些方法,通过在交互式会话中键入函数对象本身:

julia> f
f (generic function with 2 methods)

这个输出告诉我们f是有两个方法的函数对象。为了找出那些方法的signature是什么,使用 methods函数:

julia> methods(f)
# 2 methods for generic function "f":
[1] f(x::Float64, y::Float64) in Main at none:1
[2] f(x::Number, y::Number) in Main at 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)

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

这个接受所有的方法比其他的对一堆参数值的其他任意可能的方法定义更不专用。所以他只会被没有其他方法定义应用的一对参数调用。

虽然这像是一个简单的概念,基于值的类型的多重分派可能是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有能力抽象地表达高层级算法,而与实现细节解耦,也能生成高效而专用的代码来在运行中处理每个情况。

方法歧义


在一系列的函数方法定义时没有单独的最专用的方法能适用于参数的某些组合是可能的:

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) in Main at none:1
  g(x::Float64, y) in Main at none:1
Possible fix, define
  g(::Float64, ::Float64)

这里,调用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

建议首先定义消歧方法,否则歧义就会暂时存在,直到更具体的方法被定义为止。在更复杂的情况下,解决方法歧义涉及设计的某一要素;本课题将在下文进一步探讨。

方法参数(Parametric Methods)


方法定义可以有限定签名的类型参数:

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)

当两个参数都是相同的具体类型时,第一个方法就会应用,而不管是什么类型,而第二个方法充当一个CATCULL,涵盖所有其他情况。因此,总的来说,这定义了一个布尔函数,它检查其两个参数是否属于同一类型:

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

分派函数行为的这种定义非常常见-惯用的,甚至-在juliio中。方法类型参数不限于用作参数的类型:它们可以在函数的函数或主体的签名中的任何地方使用。方法符号中的参数型向量{t}

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

julia> myappend([1,2,3],4)
4-element Array{Int64,1}:
 1
 2
 3
 4

julia> myappend([1,2,3],2.5)
ERROR: MethodError: no method matching myappend(::Array{Int64,1}, ::Float64)
Closest candidates are:
  myappend(::Array{T,1}, !Matched::T) where T at none:1

julia> myappend([1.0,2.0,3.0],4.0)
4-element Array{Float64,1}:
 1.0
 2.0
 3.0
 4.0

julia> myappend([1.0,2.0,3.0],4)
ERROR: MethodError: no method matching myappend(::Array{Float64,1}, ::Int64)
Closest candidates are:
  myappend(::Array{T,1}, !Matched::T) where T at none:1

如您所见,附加元素的类型必须与它附加到的向量的元素类型相匹配,否则就会引发一种方法类型。在下面的示例中,方法类型参数t被用作返回值:

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

julia> mytypeof(1)
Int64

julia> mytypeof(1.0)
Float64

正如可以在类型声明中(参见参数类型)中对类型参数设置子类型约束一样,还可以约束方法的类型参数:

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)
Closest candidates are:
  same_type_numeric(!Matched::T<:Number, ::T<:Number) where T<:Number at none:1
  same_type_numeric(!Matched::Number, ::Number) at none:1

julia> same_type_numeric("foo", "bar")
ERROR: MethodError: no method matching same_type_numeric(::String, ::String)

julia> same_type_numeric(Int32(1), Int64(2))
false

同_type_NUMERE函数的行为与上面定义的相同类型函数非常相似,但只对数字对进行定义。

参数方法允许使用与用于写入类型的表达式相同的语法(参见unionall type)。如果只有一个参数,则可以省略括起来的大括号(在其中{t}中),但对于clarity来说通常是首选的。多个参数可以用逗号分隔,例如{t,s<:true},或者使用嵌套的地方编写,例如s<:realwhere 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

在这个例子中,观察到新的NeFun定义已经创建,但是不能立即调用。新的全局函数在TyEVAY函数中是可见的,因此可以编写返回NeFun(没有括号)。但是,你和你的呼叫者,或者他们所调用的函数,等等,都不能调用这个新的方法定义!

但是有一个例外:将来从应答工作调用new.,正如预期的那样,能够查看和调用newfun的新定义。

但是,将来对trival的调用将继续看到new乐趣的定义,就像在repl上的前一条语句中一样,因此在调用trairval之前也是如此。

您可能希望自己尝试一下,看看它是如何工作的。

这种行为的实现是一个“世界年龄计数器”。这个单调递增的值跟踪每个方法定义的操作。这允许将“对给定运行时环境可见的方法定义集”描述为单个数字,或“世界年龄”。它还允许比较两个世界中可用的方法,只需比较它们的序数值。在上面的示例中,我们看到“当前世界”(在这个世界中,方法是是一个更大的任务,本地的“运行时世界”是在开始执行时修复的。

有时有必要绕过这个问题(例如,如果您正在实现上面的repl)。幸运的是,有一个简单的解决方案:使用base.invokelatest调用函数:

julia> function tryeval2()
           @eval newfun2() = 2
           Base.invokelatest(newfun2)
       end
tryeval2 (generic function with 1 method)

julia> tryeval2()
2

最后,让我们来看一些更复杂的例子,在这些例子中,这个规则进入了Play。定义一个函数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"

参数化设计模式(Design Patterns with Parametric Methods)


虽然性能或可用性不需要复杂的调度逻辑,但有时它可能是表示某些算法的最佳方法。

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

以下是返回任意抽象类型的元素类型t的正确代码模板:

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

使用所谓的三角调度。注意,如果t是unionall类型,例如eltype(数组{t}其中t<:整数),则返回任何类型(与基中eltype的版本一样)。

在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))

一个常见的错误是尝试通过内省获得元素类型:

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

然而,不难构造出这样做会失败的情况:

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

这里我们已经创建了一个没有参数的类型位向量,但是元素类型仍然是完全指定的,t等于bool!

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

在构建泛型代码时,通常需要通过对类型的布局进行一些更改来构造类似的对象,还需要更改类型参数。例如,您可能有某种具有任意元素类型的抽象数组,并且希望用特定的元素类型在其上编写计算。我们必须为每个抽象的{t}子类型实现一个方法描述如何计算此类型转换。没有将一个子类型转换为另一个具有不同参数的子类型的一般转换。(快速回顾:您明白为什么会这样吗?)

抽象对象的子类型通常实现两种方法来实现这一点:将输入数组转换为特定抽象数组{t,n}抽象类型的子类型的方法;以及使用特定元素类型创建新的未初始化数组的方法。这些实现的示例实现可以在Julia base中找到。下面是它们的基本示例用法,确保输入和输出是相同类型的:

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

作为对此的扩展,在算法需要输入数组的副本的情况下,由于返回值可能会别名原始输入,所以转换是不够的。将类似(以输出数组)与Copitto!(用输入数据填充)组合在一起是表示输入参数的可变副本要求的一种通用方法:

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

迭代调度

为了发送一个多层次的参数论证列表,通常最好将每个级别的分派分离成不同的功能。这在单次分派的方法上听起来可能类似,但正如我们下面所看到的,它仍然更灵活。

例如,尝试对数组的元素类型进行分派通常会遇到不明确的情况。相反,通常代码会首先对容器类型进行分派,然后再恢复到基于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)

输出类型计算

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

为了实现基本操作,例如加法,我们使用Promote_type函数来计算所需的输出类型(和前面一样,我们在调用to的促进调用中看到了这一点)。

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

  • 编写一个表示算法内核执行的操作集的小函数OP。
  • 将结果矩阵的元素类型r计算为PRESS_OP(OP,参数_TYTYS.),其中参数_TYPE是根据应用于每个输入数组的eltype计算的。
  • 构建类似的输出矩阵(r,dims),其中dims是输出数组所需的维数。

对于更具体的示例,泛型方阵乘伪码可能如下所示:

function matmul(a::AbstractMatrix, b::AbstractMatrix)
    op = (ai, bi) -> ai * bi + ai * 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(b, 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 * 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)...)

参数约束的varargs方法
函数参数也可以用来约束可以提供给“varargs”函数(varargs函数)的参数。

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)
Closest candidates are:
  bar(::Any, ::Any, ::Any, !Matched::Any) at none:1

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)
Closest candidates are:
  bar(::Any, ::Any, ::Any, ::Any) at none:1

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

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的缩写。

关于可选参数和关键字参数的说明(Note on Optional and keyword Arguments)


正如在函数中简要提到的,可选的参数被实现为多个方法定义的语法。例如,这个定义:

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。换句话说,可选参数被绑定到一个函数,而不是该函数的任何特定方法。这取决于可选参数的类型。如果用全局变量定义了可选参数,那么在运行时,可选参数的类型甚至可能发生变化。

关键字参数的行为与普通的位置论证有很大的不同。特别是,它们不参与方法分派。方法仅基于位置参数分派,关键字参数在识别匹配方法后处理。

函数类对象(Function-like objects)


方法与类型相关联,因此可以通过向类型添加方法使任意Julia对象“可调用”(此类“可调用”对象有时称为“函子”)。

例如,您可以定义一种类型,它存储多项式的系数,但其行为类似于计算多项式的函数:

ulia> 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(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参数
元组(和ntuple)参数提出了特殊的挑战。

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

之所以不明确,是因为n=0:没有元素可以确定int或Float 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

正交化你的设计
当您可能想要分派两个或多个参数时,请考虑“wrapper”函数是否可以简化设计。例如,而不是写多个变体:

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

一项相关的战略利用促进将x和y带到一种共同的类型:

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

这种设计的一个风险是,如果没有合适的促进方法将x和y转换为相同的类型,则第二种方法将无限地恢复自身并触发堆栈溢出。非导出函数base.pro促进_non-循环可用作替代;升级失败时仍会抛出错误,但如果有更具体的错误消息,则会更快地失败。

一次发出一个论点

如果您需要对多个参数进行分派,并且有太多的组合使定义所有可能的变体变得实际,那么请考虑引入一个“名称级联”,在其中(例如)对第一个参数进行分派,然后调用一个内部方法:

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::..,b),并确保此方法是通过对每个容器类型和元素类型分别执行正确操作的泛型调用(类似和-)实现的。这只是对您的方法进行正交化的建议的一个更复杂的变体。

当这种方法不可能时,可能值得与其他开发人员讨论如何解决歧义;仅仅因为首先定义了一种方法并不一定意味着它不能被修改或消除。作为最后的手段,一位开发人员可以定义“带助”方法。

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

用蛮力解决歧义问题。

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

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

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

这两种方法一起产生无限递归,并且不断增长。

他最好的设计是像这样定义您的调用层次:

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与任何其他类型的填充都是在相同的参数位置提供的,因此它保持了调度层次的良好组织,并且减少了歧义的可能性。此外,它扩展了“公共”myfilter接口:希望显式控制填充的用户可以直接调用nopad变体。


完结于 2018-08-30 21:38

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值