Efficient way to implement multiple dispatch for many similar functions

297 views Asked by At

I am writing some software that involves a library of various functional forms of a quantity. I want to leverage Julia's multiple dispatch, but want to know if there's a more efficient way to implement this procedure.

Consider, for example, a library that contains the following two functions

function firstfunction(x::Float64)
    return 2*x
end

function secondfunction(x::Float64)
    return x^2
end

I would also like to implement multiple dispatch methods that can apply these functional forms to an vector of values, or an array of vectors (matrix). I could do this as follows

function firstfunction(x::Float64)
    return 2*x
end

function firstfunction(xs::Vector{Float64})
    f = similar(xs)
    for i = 1:size(xs, 1)
        f[i] = 2*xs[i]
    end
    return f
end

function firstfunction(xss::Matrix{Float64})
    f = similar(xss)
    for i = 1:size(xss, 1)
        for j = 1:size(xss, 2)
            f[i, j] = 2*xss[i, j]
    end
    return f
end

function secondfunction(x::Float64)
    return x^2
end

function secondfunction(xs::Vector{Float64})
    f = similar(xs)
    for i = 1:size(xs, 1)
        f[i] = xs[i]^2
    end
    return f
end

function secondfunction(xss::Matrix{Float64})
    f = similar(xss)
    for i = 1:size(xss, 1)
        for j = 1:size(xss, 2)
            f[i, j] = xss[i, j]^2
    end
    return f
end

But since all three versions of the function use the same kernel, and the actions of the various dispatches are the same across all functional forms, I'd like to know if there's a more efficient way to write this such that defining a new function for the library (e.g thirdfunction) only involves explicitly writing the kernel function, rather than having to type out 2*n essentially identical functions for n functional forms in the library.

2

There are 2 answers

0
Przemyslaw Szufel On BEST ANSWER

Just do:

function thirdfunction(x::Union{Number, Array{<:Number}})
    return x.^0.5
end

This is the beauty of multiple-dispatch in Julia:

julia> thirdfunction(4)
2.0

julia> thirdfunction([4,9])
2-element Array{Float64,1}:
 2.0
 3.0

julia> thirdfunction([4 9; 16 25])
2×2 Array{Float64,2}:
 2.0  3.0
 4.0  5.0

Note that however in your case it might make sense to have only a single representation of a function and let the user decide to vectorize it using the dot operator (.).

function fourthfunction(x::Real)
    min(x, 5)
end

And now the user just needs to add a dot when needed:

julia> fourthfunction(4)
4

julia> fourthfunction.([4,9])
2-element Array{Int64,1}:
 4
 5

julia> fourthfunction.([4 9; 16 25])
2×2 Array{Int64,2}:
 4  5
 5  5

Since vectorizing in Julia is so easy you should consider this design whenever possible,

2
lungben On

You should not use type annotations if they are not required for safety or multiple dispatch. E.g. it is unlikely that firstfunction should only work for Float64, probably it should work for all numbers, therefore write

function firstfunction(x::Real) # or just firstfunction(x)
    return 2*x
end

There is no performance penalty for defining functions more general.

Back to the topic: For applying a function to a matrix / vector, etc. the simplest way is to use broadcasting:

A = rand(10, 10) # 10x10 matrix
B = firstfunction.(A) # apply element-wise

If you want to define the way how to apply another function by yourself, you can use functions as input parameters, e.g.:

thirdfunction(f, x) = f(f(x))