如何在处理多种类型和数组时编写“优秀”的Julia代码(多重派发)

40

OP更新:请注意,在最新版本的Julia(v0.5)中,回答这个问题的惯用方法是只需定义mysquare(x :: Number) = x ^ 2 。使用自动广播覆盖向量化案例,即x = randn(5); mysquare .(x)。还可以查看解释点语法更详细的新答案。

我是Julia的新手,由于我的Matlab背景,我很难确定如何编写“好”的Julia代码,以利用多重分派和Julia的类型系统。

考虑一个函数,它提供一个Float64的平方。我可能会将其编写为:

function mysquare(x::Float64)
    return(x^2);
end

有时候,我想对一维数组中的所有Float64进行平方操作,但不想每次都写一个循环来执行mysquare函数,因此我使用了多重派发,并添加了以下代码:
function mysquare(x::Array{Float64, 1})
    y = Array(Float64, length(x));
    for k = 1:length(x)
        y[k] = x[k]^2;
    end
    return(y);
end

但现在我有时使用Int64,因此我编写了另外两个函数来利用多分派:

function mysquare(x::Int64)
    return(x^2);
end
function mysquare(x::Array{Int64, 1})
    y = Array(Float64, length(x));
    for k = 1:length(x)
        y[k] = x[k]^2;
    end
    return(y);
end

这样做对吗?还是有更常见的处理方法?我应该像这样使用类型参数吗?

function mysquare{T<:Number}(x::T)
    return(x^2);
end
function mysquare{T<:Number}(x::Array{T, 1})
    y = Array(Float64, length(x));
    for k = 1:length(x)
        y[k] = x[k]^2;
    end
    return(y);
end

这种做法看起来很合理,但是我使用参数化类型会使我的代码运行速度和避免使用参数化类型的情况一样快吗?
总之,我的问题分为两个部分:
1. 如果快速代码对我很重要,我应该像上面描述的那样使用参数化类型,还是应该为不同的具体类型编写多个版本?或者我应该完全采用其他方法?
2. 当我想要一个可以操作数组和标量的函数时,是否编写两个版本的函数,一个用于标量,另一个用于数组,是良好的实践?或者我应该完全采用其他方法?
最后,请指出您认为以上代码中的任何其他问题,因为我的最终目标是编写优秀的Julia代码。
2个回答

43

Julia会根据需要为每组输入编译特定版本的函数。因此,回答第一部分问题时,没有性能差异。参数化方式是正确的方法。

至于第二部分,有时编写单独的版本(有时出于性能原因,例如避免复制)可能是一个好主意。然而,在您的情况下,您可以使用内置宏@vectorize_1arg自动生成数组版本,例如:

function mysquare{T<:Number}(x::T)
    return(x^2)
end
@vectorize_1arg Number mysquare
println(mysquare([1,2,3]))

通常情况下,不要使用分号,mysquare(x::Number) = x^2 更为简短。

对于您的向量化函数mysquare,考虑TBigFloat的情况。然而,您的输出数组是Float64。处理这种情况的一种方法是将其更改为

function mysquare{T<:Number}(x::Array{T,1})
    n = length(x)
    y = Array(T, n)
    for k = 1:n
        @inbounds y[k] = x[k]^2
    end
    return y
 end

我已经添加了@inbounds宏来提高速度,因为我们不需要每次都检查边界违规 - 我们知道长度。 但是,如果x[k]^2的类型不是T,则此函数仍然可能存在问题。更加严谨的版本可能是

function mysquare{T<:Number}(x::Array{T,1})
    n = length(x)
    y = Array(typeof(one(T)^2), n)
    for k = 1:n
        @inbounds y[k] = x[k]^2
    end
    return y
 end

其中one(T)会在TInt时返回1,在TFloat64时返回1.0,以此类推。这些考虑只有在您想要创建超级健壮的库代码时才会有影响。如果您只处理Float64或可以提升为Float64的内容,则不是问题。这似乎很费力,但其威力非凡。您始终可以接受类似Python的性能,并忽略所有类型信息。


1
为什么要使用mysquare{T<:Number}(x::T)而不是mysquare(x::Number) - Adobe
没有什么特别的原因,只是为了与向量化版本在视觉上保持一致。一般情况下,我不会这样写。 - IainDunning
6
您可能需要针对向量化更改更新此内容。这会在侧边栏中弹出,因为它得到了很高的赞同,因此搜索的人可能会发现@vectorize_1arg,而现在应使用.符号。 - Chris Rackauckas

6
截至Julia 0.6(约为2017年6月),"点语法"提供了一种简单和惯用的方法来将函数应用于标量或数组。
您只需要提供以正常方式编写的函数的标量版本。
function mysquare{x::Number)
    return(x^2)
end

在函数名称后面添加一个.(或在运算符前面添加)以调用其在数组的每个元素上执行的功能:

x = [1 2 3 4]
x2 = mysquare(2)     # 4 
xs = mysquare.(x)    # [1,4,9,16]
xs = mysquare.(x*x') # [1 4 9 16; 4 16 36 64; 9 36 81 144; 16 64 144 256]
y  = x .+ 1          # [2 3 4 5]

请注意,点调用将处理广播,就像最后一个示例中那样。
如果在同一表达式中有多个点调用,它们将被融合,以便 y = sqrt.(sin.(x)) 只需进行一次传递/分配,而不是创建一个包含 sin(x) 并将其转发到 sqrt() 函数的临时表达式。(这与 Matlab/Numpy/Octave/Python/R 不同,它们不提供这样的保证)。
@. 使每行向量化,因此 @. y=sqrt(sin(x))y = sqrt.(sin.(x)) 相同。对于多项式,这尤其方便,其中重复的点可能会令人困惑...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接