返回介绍

方法

发布于 2019-07-03 15:53:49 字数 5635 浏览 1020 评论 0 收藏 0

我们回想一下,在[函数]

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

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会扔出MethodError而非任意选择一个方法。你可以通过对交叉情况指定一个合适的方法来避免方法歧义:

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的方法多态性是其最有力的特性之一,利用这个功能会带来设计上的挑战。特别地,在更加复杂的方法层级中出现[歧义](http://127.0.0.5/@ref man-ambiguities)不能说不常见。

在上面我们曾经指出我们可以像这样解决歧义

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

靠定义一个方法

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

这是经常使用的对的方案;但是有些环境下盲目地遵从这个建议会适得其反。特别地,范用函数有的方法越多,出现歧义的可能性越高。当你的方法层级比这些简单的例子更加复杂时,就值得你花时间去仔细想想其他的方案。

下面我们会讨论特别的一些挑战和解决这些挑战的一些可选方法。

元组和N元组参数

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

[正交化你的设计](@id man-methods-orthogonalize)

当你打算根据两个或更多的参数进行分派时,考虑一下,一个「包裹」函数是否会让设计简单一些。举个例子,与其编写多变量:

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最可能需要一个fallback定义

g(x::A) = x

一个相关的方案使用promote来把xy变成常见的类型:

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

这个设计的一个隐患是如果没有合适的把xy转换到同样类型的类型提升方法,第二个方法就可能无限自递归然后引发堆溢出。非输出函数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-)实现的。这只是建议[正交化](http://127.0.0.5/@ref man-methods-orthogonalize)你的方法的一个更加复杂的变种而已。

当这个方法不可行时,这就值得与其他开发者开始讨论如果解决歧义;只是因为一个函数先定义并不总是意味着他不能改变或者被移除。作为最后一个手段,开发者可以定义"创可贴"方法

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

可以暴力解决歧义。

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

如果你定义了提供默认的方法"级联",要小心去掉对应着潜在默认的任何参数。例如,假设你在写一个数字过滤算法,你有一个通过应用padding来出来信号的边的方法:

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

这会与提供默认padding的方法产生冲突:

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 被置于与其他 padding 类型一致的参数位置上,这保持了分派层级的良好组织,同时降低了歧义的可能性。而且,它扩展了「公开」的 myfilter 接口:想要显式控制 padding 的用户可以直接调用 NoPad 变量。

[^Clarke61]: Arthur C. Clarke, Profiles of the Future (1961): Clarke's Third Law.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文