Reuse `Tuple` `convert` mechanism in another parametric type

Tuples have this nice behavior:

z8 = (Int8(0),Int8(0))
z64 = (0,0)

a8 = Vector{Tuple{Int8, Int8}}()
a64 = Vector{Tuple{Int64, Int64}}()

# these all work
push!(a8, z8)
push!(a64, z64)
push!(a8, z64)
push!(a64, z8)

At first I thought the behavior I wanted was related to Tuples being covariant in their type parameters [1] (which doesn’t make sense because there’s no subtype relationship between Int8 and Int64, and I would also need contravariance at the same time). But instead he behavior I want is due to the convert methods defined on Tuple [2].

# previous `push!`s work because these all work
convert(eltype(a8), z8)
convert(eltype(a64), z64)
convert(eltype(a8), z64)
convert(eltype(a64), z8)

I would like to apply this behavior to another type I define:

struct Wrap{T}
  x::T
end

w8 = Wrap(z8)
w64 = Wrap(z64)

b8 = Vector{Wrap{Tuple{Int8, Int8}}}()
b64 = Vector{Wrap{Tuple{Int64, Int64}}}()

# I want all of these to work
push!(b8, w8)
push!(b64, w64)
push!(b8, w64)
push!(b64, w8)

# which requires all of these to work.
convert(eltype(b8), w8)
convert(eltype(b64), w64)
convert(eltype(b8), w64)
convert(eltype(b64), w8)

I can get my desired Wrap behavior by defining

Base.convert(::Type{Wrap{T}}, w::Wrap) where {T} = Wrap(convert(T, w.x))

but I’m hoping there’s an existing idiom for defining a similar method across all fields of a given type.

Related discussion:

[1] Types · The Julia Language
[2] https://github.com/JuliaLang/julia/blob/2d5741174ce3e6a394010d2e470e4269ca54607f/base/essentials.jl#L301-L314

Generally you would need a convert method.

Are you looking for an easier way to implement that? Is there a more complex example you need help with? I am asking because for the above, what you are doing may be the simplest solution.

I think you understand already, and I’m just looking for a package or some other abbreviated way to do the following:

function Base.convert(::Type{Wrap3{A, B, C}}, w::Wrap3) where {A, B, C}
  return Wrap3(
    convert(A, w.a),
    convert(B, w.b),
    convert(C, w.c)
  )
end

given

struct Wrap3{A, B, C}
  a::A
  b::B
  c::C
end

Seems like it might exist in a package as a macro definition.

Another two “features” I’d like:

  1. free type parameters
convert(Tuple{Int64, A} where A, (1.0, 2.0))

makes a (1, 2.0)

Analogously, I’d like to have this work:

convert(Wrap3{Int64, Float32, C} where C, Wrap3(1.0, 2, "hey"))
  1. type parameters that cannot be inferred from constructor arguments:
    I can’t demonstrate this with Tuple, but I would hope that whatever mechanism gets me the previously mentioned features would also work as follows
struct TaggedWrap3{TAG, A, B, C}
  a::A
  b::B
  c::C
end

# allow construction like: TaggedWrap3{:tag}(1,2,3)
function (::Type{TaggedWrap3{TAG,A,B,C} where {A,B,C}})(a,b,c) where {TAG}
  return TaggedWrap3{TAG, typeof(a), typeof(b), typeof(c)}(a,b,c)
end

function Base.convert(::Type{TaggedWrap3{TAG,A,B,C} where {A, B, C}}, w::Wrap3) where {TAG}
  return TaggedWrap3{TAG}(w.a, w.b, w.c)
end

function Base.convert(::Type{TaggedWrap3{TAG,A,B,C}}, w::Wrap3) where {TAG, A, B, C}
  return TaggedWrap3{TAG}(
    convert(A, w.a),
    convert(B, w.b),
    convert(C, w.c)
  )
end

function Base.convert(::Type{TaggedWrap3{Any, A, B, C}}, w::TaggedWrap3{TAG}) where {TAG, A, B, C}
  return TaggedWrap3{TAG}(
    convert(A, w.a),
    convert(B, w.b),
    convert(C, w.c)
  )
end

@show convert(TaggedWrap3{:tag}, Wrap3(1,2,3))
@show convert(TaggedWrap3{:tag, Float64, Float64, Float64}, Wrap3(1,2,3))
@show convert(TaggedWrap3{Any, Float64, Float64, Float64}, TaggedWrap3{:tag}(1,2,3))
# doesn't work, but should
@show convert(TaggedWrap3{:tag, Float64, Float64, C} where C, Wrap3(1,2,3))

producing

convert(TaggedWrap3{:tag}, Wrap3(1, 2, 3)) = TaggedWrap3{:tag,Int64,Int64,Int64}(1, 2, 3)
convert(TaggedWrap3{:tag, Float64, Float64, Float64}, Wrap3(1, 2, 3)) = TaggedWrap3{:tag,Float64,Float64,Float64}(1.0, 2.0, 3.0)
convert(TaggedWrap3{Any, Float64, Float64, Float64}, TaggedWrap3{:tag}(1, 2, 3)) = TaggedWrap3{:tag,Float64,Float64,Float64}(1.0, 2.0, 3.0)

I am not aware of a package like this, but note that the default constructor goes a long way:

struct Wrap3{A,B,C}
    a::A
    b::B
    c::C
end

Base.convert(::Type{W}, w::Wrap3) where {W <: Wrap3} = W(w.a, w.b, w.c)

convert(Wrap3{Int,Float64,Int8}, Wrap3(1, 2, 3)) # example

You can do the same for TaggedWrap, just use the W{TAG} constructor.