For this particular function f it does not matter what the order of the arguments is, meaning f(x::TypeA, y::TypeB, z::TypeC) == f(::TypeB, y::TypeC, z::TypeA) == f(::TypeC, y::TypeA, z::TypeB) etc.
How can I make f work on every combination of arguments without having to write out every single one by hand? Dispatching on a single abstract type would not work because f could have multiple methods, each operating on different types.
Can you give a concrete description of the function you are trying to implement? A lot of the examples where argument-order doesn’t matter fall into one of two categories:
Commutative numerical operations on numerical arguments (like + or hypot). In this case you either promote them to a common type with f(args...) = f(promote(args)...) or (in cases where mixed types can be handled more efficiently, like real + complex) you canonicalize the order.
Cases where you should probably use keyword arguments instead.
This particular example came up when working on Copulas.jl.
I have to compute a correction factor, based on two input marginal distributions. The generic case requires a quadrature and a bisection, which is expensive. But there are closed form solutions for a number of special cases.
Essentially:
# closed-form problems
function _nataf_problem(::Distributions.Normal, Fⱼ::Distributions.LogNormal,
ρ::Real, nodes::Integer)
# r(ρ₀) = ρ₀ s/√(exp(s²) - 1)
...
end
function _nataf_problem(::Distributions.Uniform, ::Distributions.Normal,
ρ::Real, nodes::Integer)
# r(ρ₀) = ρ₀ √(3/π).
...
end
... list goes on with more special cases
But the order of the marginals doesn’t matter, so you also need the reverse:
# now reserve the order and forward
function _nataf_problem(Fᵢ::Distributions.LogNormal, Fⱼ::Distributions.Normal,
ρ::Real, nodes::Integer)
return _nataf_problem(Fⱼ, Fᵢ, ρ, nodes)
end
function _nataf_problem(Fᵢ::Distributions.Normal, Fⱼ::Distributions.Uniform,
ρ::Real, nodes::Integer)
return _nataf_problem(Fⱼ, Fᵢ, ρ, nodes)
end
... etc
For two arguments, this is of course doable to write out. But for more arguments it quickly explodes. So I was thinking, is there a general way to handle this, even if you had more arguments? I could not find one easily…
Positional arguments are fundamentally distinguished by order, and there’s no stable static sort for types to get around this. Two not-great options off the top of my head:
Runtime sort a vector of distributions by x -> objectid(typeof(x)). That’s an internal detail and can easily change across versions or perhaps even processes, so you wouldn’t want this to be static. Then again, you wouldn’t want this at runtime if you want this to be optimized.
@eval loop for each correction factor formula over permutations of an n-tuple of marginals annotation symbols. Yes, this metaprogramming blows up to factorial(n) methods like the manually forwarding methods, but at least you’re only writing out the template.
julia> begin
using Combinatorics: permutations
struct A end
struct B end
for (x,y) in permutations((:A, :B))
@eval f(::$x, ::$y) = "AB"
end
f(A(),B()), f(B(),A())
end
("AB", "AB")
Thanks. Yes, this is fighting positional dispatch which is why it’s so awkward to handle. I was somewhat hoping someone else ran into this problem maybe years ago already and came up with a generic solution that I just missed. It seems broad enough that it would come up more often, but alas?
For the two-argument case I did find this solution using Core.applicable: