我看了一下英文社区的解释以及文档的解释,似乎有一点太硬了,不是特别容易理解。
让我来尝试解释的话,可以用下面三个例子作为对比:
# case 1: T 是具体类型:v 的元素类型以及 x 和 y 的类型严格相同
# case 2: T 是抽象类型,等价于 `myappend_1(v::Vector{Real}, x::Real, y::Real)`
function myappend_1(v::Vector{T}, x::T, y::T) where {T}
return [v..., x, y]
end
# v 的元素类型以及 x 和 y 的类型严格相同
# 注:抽象类型是无法实例化的,因此 Julia 编译的函数体中 x, y 所对应的 T 一定是具体类型
function myappend_2(v::Vector{<:T}, x::T, y::T) where {T}
return [v..., x, y]
end
# x, y 的元素类型必须相同
function myappend_3(v::Vector{T}, x::S, y::S) where {T, S<:T}
return [v..., x, y]
end
julia> myappend_1(Real[1, 2, 3], 4.0, 5)
5-element Vector{Float64}:
1.0
2.0
3.0
4.0
5.0
julia> myappend_2(Real[1, 2, 3], 4.0, 5)
ERROR: MethodError: no method matching myappend_2(::Vector{Real}, ::Float64, ::Int64)
Closest candidates are:
myappend_2(::Vector{<:T}, ::T, ::T) where T
@ Main REPL[3]:1
Stacktrace:
[1] top-level scope
@ REPL[6]:1
julia> myappend_3(Real[1, 2, 3], 4.0, 5)
ERROR: MethodError: no method matching myappend_3(::Vector{Real}, ::Float64, ::Int64)
Closest candidates are:
myappend_3(::Vector{T}, ::S, ::S) where {T, S<:T}
@ Main REPL[4]:1
Stacktrace:
[1] top-level scope
@ REPL[7]:1
可以开一个新的 Julia 进程,然后用 MethodAnalaysis 来看一下第一个方法的两种编译结果:
julia> using MethodAnalysis
julia> function myappend_1(v::Vector{T}, x::T, y::T) where {T}
return [v..., x, y]
end
myappend_1 (generic function with 1 method)
julia> methodinstances(myappend_1)
Core.MethodInstance[]
julia> myappend_1(Real[1, 2, 3], 4.0, 5);
julia> methodinstances(myappend_1)
1-element Vector{Core.MethodInstance}:
MethodInstance for myappend_1(::Vector{Real}, ::Float64, ::Int64)
会发现 Julia 编译的函数体是 myappend_1(::Vector{Real}, ::Float64, ::Int64)
.
如果我们将 T
理解成占位符的话,它的结果其实等价于直接调用下面函数定义的结果:
function myappend_1(v::Vector{Real}, x::Real, y::Real)
return [v..., x, y]
end
Side note
- 在实际应用中,第二种写法是最普遍的。
因为它与 Julia 的性能最佳实践是一致的:鼓励用 Vector{Int}
而不是 Vector{Real}
.
function myappend_2(v::Vector{<:T}, x::T, y::T) where {T}
return [v..., x, y]
end
像第一种场景中 myappend_1(Real[1, 2, 3], 4.0, 5)
work 或者不 work,在实际应用中我基本都不会关心。
- 对于一些容易搞混的跨层级类型约束的声明,尽量引入辅助派发来逐层分解。例如:
myappend_1(v::Vector{T}, x::T, y::T) where {T} = ...
可以拆分为两层派发:
myappend_1(v::AbstractArray, x, y) = _myappend_1(eltype(v), v, x, y)
_myappend_1(::Type{T}, v::AbstractArray, x::T, y::T) where {T} = [v..., x, y]
即:第一层派发将矩阵的元素类型提取出来,然后放在第二层派发来统一处理。
这样从阅读的角度来说大概更容易理解一些。
例如:JuliaImages 中有一个非常有趣的 imfilter
分层派发的实现
# Step 1: if necessary, determine the output's element type
@inline function imfilter(img::AbstractArray, kernel, args...)
imfilter(filter_type(img, kernel), img, kernel, args...)
end
# Step 2: if necessary, put the kernel into cannonical (factored) form
@inline function imfilter(::Type{T}, img::AbstractArray, kernel::Union{ArrayLike,Laplacian}, args...) where {T}
imfilter(T, img, factorkernel(kernel), args...)
end
@inline function imfilter(::Type{T}, img::AbstractArray{TI}, kernel::AbstractArray{TK}, args...) where {T<:Integer,TI<:Integer,TK<:Integer}
imfilter(T, img, (kernel,), args...)
end
...
- Julia 有一些复杂的语法写法,并不代表我们需要使用它。类型体操不是什么好玩的东西。