Breaking circularity in fallback methods

I have two functions f(x, y) and g(x, y). For some types of x, it may be advantageous to reuse components of calculations when both are needed, for some other types this is not the case. On the other hand, sometimes I just need f or g.

I could define

fg(x, y) = f(x,y), g(x, y)
f(x, y) = fg(x,y)[1]
g(x, y) = fg(x,y)[2]

as fallback methods, and then either define

fg(x::SomeType, y) = ...

or

f(x::SomeOtherType, y) = ...
g(x::SomeOtherType, y) = ...

The problem is that if I have a missing method, I get a StackOverflowError. Is there a way to avoid this, and break the circularity when I don’t have the right methods?

I’ve been battling this a lot recently, I hope you can get a good way for handling that!

What about this trick?

fg(x, y, undef=false) = if undef "stack","overflow" else f(x,y), g(x, y) end;

f(x, y, undef=true) = fg(x,y,undef)[1]
g(x, y, undef=true) = fg(x,y,undef)[2]

You could overwrite without undef argument:

f(x::Int, y) = 0
g(x::Int, y) = 0

fg(1,1) # -> (0, 0)
fg(1.,1)  # -> ("stack", "overflow")

1 Like

This works, but AFAICT has a costly method table lookup. Can this be optimized away somehow?

@inline function fallback(fun, x::T) where T
    if which(fun, Tuple{T}) ≡ which(fun, Tuple{Any})
        error("no fallback method `$fun` for `$T`")
    else
        fun(x)
    end
end

f(x) = fallback(fg, x)[1]

g(x) = fallback(fg, x)[2]

fg(x) = fallback(f, x), fallback(g, x)

f(x::Float64) = x + 1

g(x::Float64) = zero(x)

fg(x::Int) = x^2, 2*x

The costly method lookup is just from type instability. Change it to

function fg(x, y, undef=false)
    undef && error("You must define either `fg` or `f` and/or `g` for inputs ", x, " and ", y)
    f(x, y), g(x, y)
end

and the performance hit should go away.

1 Like