Using invoke() to call more general method

I have a method for convert() that I use on various subtypes of a ConfigStruct type, to initialize from a dictionary. For a couple of subtypes, I want to define specific behaviors (e.g., catch deprecated keys and map them to their updated names), then call the general method. Here’s the idea, pared way down:

import Parameters: @with_kw

abstract type ConfigStruct end

@with_kw struct C1 <: ConfigStruct
    a::String
end

@with_kw struct C2 <: ConfigStruct
    b::String
end

function Main.convert(::Type{T}, d::Dict{Symbol}) where {T<:ConfigStruct}
    @info "Converting $T, generic"
    return T(; d...)
end

function Main.convert(::Type{T}, d::Dict{Symbol}) where {T<:C2}
    @info "Converting $T, specific"
    # ...convert, then call other method
    return invoke(convert, Tuple{Type{ConfigStruct},Dict{Symbol}}, (T, d))
end

s1::C1 = Dict(:a => "a")
s2::C2 = Dict(:b => "b")

Unfortunately, this usage of invoke() doesn’t work, I get this error on the final line of code:

[ Info: Converting C2, specific
ERROR: invoke: argument type error

I’ve tried several variations on that invoke(), does anyone have any guidance?

This seems to work:

function Base.convert(::Type{T}, d::Dict{Symbol}) where {T<:C2}
    @info "Converting $T, specific"
    # ...convert, then call other method
    return invoke(convert, Tuple{Type{<:ConfigStruct}, Dict{Symbol}}, T, d)
end

However, I would probably just use constructors. This seems too cute by half:

c1::C1 = Dict(:a => "a")
c2::C2 = Dict(:b => "b")

This looks cleaner and clearer to me:

c1 = C1(a="a")
c2 = C2(b="b")

Regular Julia code rarely needs to invoke invoke.

1 Like

Also of note, there’s a more convenient macro form since Julia 1.7:

help?> @invoke
  @invoke f(arg::T, ...; kwargs...)

  Provides a convenient way to call invoke by expanding @invoke f(arg1::T1, arg2::T2; kwargs...) to invoke(f, Tuple{T1,T2}, arg1, arg2; kwargs...). When an argument's type annotation is
  omitted, it's replaced with Core.Typeof that argument. To invoke a method where an argument is untyped or explicitly typed as Any, annotate the argument with ::Any.

  It also supports the following syntax:

    •  @invoke (x::X).f expands to invoke(getproperty, Tuple{X,Symbol}, x, :f)

    •  @invoke (x::X).f = v::V expands to invoke(setproperty!, Tuple{X,Symbol,V}, x, :f, v)

    •  @invoke (xs::Xs)[i::I] expands to invoke(getindex, Tuple{Xs,I}, xs, i)

    •  @invoke (xs::Xs)[i::I] = v::V expands to invoke(setindex!, Tuple{Xs,V,I}, xs, v, i)

  Examples
  ≡≡≡≡≡≡≡≡

  julia> @macroexpand @invoke f(x::T, y)
  :(Core.invoke(f, Tuple{T, Core.Typeof(y)}, x, y))
  
  julia> @invoke 420::Integer % Unsigned
  0x00000000000001a4
  
  julia> @macroexpand @invoke (x::X).f
  :(Core.invoke(Base.getproperty, Tuple{X, Core.Typeof(:f)}, x, :f))
  
  julia> @macroexpand @invoke (x::X).f = v::V
  :(Core.invoke(Base.setproperty!, Tuple{X, Core.Typeof(:f), V}, x, :f, v))
  
  julia> @macroexpand @invoke (xs::Xs)[i::I]
  :(Core.invoke(Base.getindex, Tuple{Xs, I}, xs, i))
  
  julia> @macroexpand @invoke (xs::Xs)[i::I] = v::V
  :(Core.invoke(Base.setindex!, Tuple{Xs, V, I}, xs, v, i))
1 Like

Ahh thanks, I don’t know why I thought the arguments themselves also had to be in a tuple.

The following also works and I’ll use it for my general case:

invoke(convert, Tuple{Type{<:supertype(T)},typeof(d)}, T, d)

Yes, I usually would too. I’m using this technique in a context where the Dict values come from a TOML file, and they need to be recursively converted to the types that the rest of the code will use. It’s turned out to be a lot cleaner to do this implicitly by declaration of types in the ConfigStruct subtypes, rather than a lot of explicit conversion code that “knows” the details of those structures.

1 Like

Cool, thanks, that means I can change

invoke(convert, Tuple{Type{<:supertype(T)},typeof(d)}, T, d)

to

@invoke convert(T::Type{<:supertype(T)}, d)

, which reads a lot better.