I have a parametric function as discussed here and now want to generate another such function type that logs its arguments while invoking the non-logging method for the computation. How do I use invoke to dispatch on the type of f and not on argtypes?
If that is all a bit convoluted, here is a MWE
abstract type AbstractAdder <: Function end
# the non-logging version
struct Adder <: AbstractAdder
a::Int
end
# general implementation for all adders
function (f::AbstractAdder)(x::Int)
return f.a + x
end
# the logging version
struct TrackingAdder <: AbstractAdder
a::Int
log::Vector{Int}
end
# special implementation for the logging version.
# It should just push `x` to `f.log` and then call the general
# method to do the actual computation
function (f::TrackingAdder)(x::Int)
push!(f.log, x)
# how to use `invoke` here s.t. it uses the general method for `AbstractAdder`?
return invoke((), Tuple{AbstractAdder, Int}, x)
end
While I am still waiting for answers, here is what I do for now:
abstract type AbstractAdder <: Function end
# the non-logging version
struct Adder <: AbstractAdder
a::Int
end
@inline function _call(f::AbstractAdder, x::Int)
return f.a + x
end
(f::AbstractAdder)(x::Int) = _call(f, x)
# the logging version
struct TrackingAdder <: AbstractAdder
a::Int
log::Vector{Int}
end
function (f::TrackingAdder)(x::Int)
push!(f.log, x)
return _call(f, x)
end
I think that you could add dummy unused singleton arguments to either the struct or the abstract type function or both. So you ignore the value, and just let Julia use the type of the arg for dispatch.
abstract type AbstractAdder <: Function end
abstract type DisambigSingleton end
struct DisambigAbstractAdder <: DisambigSingleton end
struct DisambigTrackingAdder <: DisambigSingleton end
# the non-logging version
struct Adder <: AbstractAdder
a::Int
end
# general implementation for all adders
function (f::AbstractAdder)(x::Int, ::DisambigAbstractAdder)
return f.a + x
end
# the logging version
struct TrackingAdder <: AbstractAdder
a::Int
log::Vector{Int}
end
# special implementation for the logging version.
# It should just push `x` to `f.log` and then call the general
# method to do the actual computation
function (f::TrackingAdder)(x::Int, ::DisambigTrackingAdder)
push!(f.log, x)
return invoke(f, Tuple{Int, DisambigAbstractAdder}, x, DisambigAbstractAdder())
end
I think it would be possible to make this prettier with some wrappers, but I’m guessing you’d do best to adopt a different approach. There is probably a more “Julian” solution to your problem.
EDIT: I think an improvement might be to make DisambigSingleton a parametric struct, parameterized by, e.g., Int, then the “super” operation could perhaps be more automatic (by doing something like DisambigSingleton{N + 1} (or DisambigSingleton{N - 1}) in the methods instead of super.
Yep, that works. I modified the idea a bit more by making the singleton a default argument
function (f::AbstractAdder)(x::Int, _::DisambigAbstractAdder=DisambigAbstractAdder())
return f.a + x
end
function (f::TrackingAdder)(x::Int, _::DisambigTrackingAdder=DisambigTrackingAdder())
push!(f.log, x)
return invoke(f, Tuple{Int, DisambigAbstractAdder}, x, DisambigAbstractAdder())
end
because having to pass the singleton manually pretty much defeats the purpose of the whole exercise.