Automatically reimplement methods from one package to support equivalent type

Recently i’m have encountered an issue where i needed two packages. Package A exports a bunch of useful methods from some type T (also exported from A), and another package B that exported additional methods and a equivalent type S (meaning we can convert from T to S). The problem is that the methods from package A only accepts arguments with type T and not S. The most obvious solution is to explicit convert each object of type S to T before passing it to the methods. This can be very annoying some times, and another solution is to reimplement these methods for type S. Now the problem is if the number of methods exported by package A is very large, which can make the second solution unfeasible. To address this issue, i created a macro that automatically reimplement these methods for a specific type. Basicly, swapping the types in the arguments of each method and converting it in the function body before passing it to the method. This works well for now, but i’m pretty sure that there’s something wrong with it, and maybe in some situations it will break. Is there more efficients ways to accomplish the same thing, or nor even do this in the first place?

Here is the code for the macro (i have no idea how to name this thing):

macro remethods(_module, type1, type2)
	quote
		for name in names($_module)
			if isdefined($_module, name)
				obj = getfield($_module, name)
				if obj isa Function
					new_methods = []
					fmethods = filter(m -> hasfield(typeof(m.sig), :types), methods(obj))

					for method in fmethods
						ftype, arg_types... = method.sig.types
						name = method.name
						arg_types = collect(arg_types)
						indices = findall(==($type1), arg_types)
						if !isempty(indices)
							push!(new_methods, (; name, arg_types, indices ))
						end
					end
			
					for method in new_methods
						(; name, arg_types, indices) = method
						foreach(i -> setindex!(arg_types, $type2, i) , indices)
						arg_names = [gensym(:arg) for a in arg_types]
						
						_args = [:($name::$T) for (name, T) in zip(arg_names, arg_types)]
						_body = map(indices) do i
							argname = arg_names[i]
							:( $argname = convert($$type1, $argname) )
						end
						
						eval(:(
							function $($_module).$(name)($(_args...); kwargs...)
								$(_body...)
								$($_module).$(name)($(arg_names...); kwargs...)
							end
						))
					end
				end
			end
		end
	end |> esc
end

Simple example:

module A
           export Person
           export introduce

           struct Person
               first_name::String
               last_name::String
           end

           introduce(p::Person) = println("Hi, my name is $(p.first_name) $(p.last_name)")
       end # module A
Main.A

julia> module B
           export Android

           struct Android
               name::String
               model::String
           end
       end # module B~
Main.B

julia> using .A, .B

julia> Jhon = Person("John", "Connor")
Person("John", "Connor")

julia> Connor = Android("Connor", "RK800")
Android("Connor", "RK800")

julia> introduce(Jhon)
Hi, my name is John Connor

julia> introduce(Connor)
ERROR: MethodError: no method matching introduce(::Android)
Closest candidates are:
  introduce(::Person) at REPL[2]:10
Stacktrace:
 [1] top-level scope
   @ REPL[8]:1

julia> Base.convert(::Type{Person}, a::Android) = Person(a.name, "")

julia> @remethods A A.Person B.Android

julia> introduce(Connor)
Hi, my name is Connor

The above is just a simple example, my original problem was to use Luxor with types from GeometryBasics.

Edit: just fixed some obvious mistakes in the macro

1 Like