# How not to commit type piracy when handling automatic conversion for field with Union type

I’d like to handle the conversion in structs just like when you have a concrete type for a field, but when a field as a `Union` type like below:

``````struct A end

struct B
x::Union{A, Int}
end
``````

I’d like this to work:

``````B(true) # should return B(1)
``````

I was able to make everything work with:

``````# note: these definitions are not entirely correct in any case, see the issue for more details
Base.convert(::Type{Union{A,T}}, x) where T = convert(T, x)
Base.convert(::Type{Union{A,T}}, x::T) where T = convert(T, x)
Base.convert(::Type{Union{A,T}} where T, x::A) = x
``````

everything seems to work very well

``````julia> b1 = B(A())
B(A())

julia> b2 = B(1)
B(1)

julia> b3 = B(true) # automatic conversion
B(1)
``````

but then I was informed by @jameson in this issue: Ambiguity in Base.convert on 1.8/1.9 but not on 1.10 and master · Issue #53224 · JuliaLang/julia · GitHub that I’m committing type-piracy, and indeed this now fails!

``````julia> Union{Nothing, Union{A, Int}}[1][1]
ERROR: MethodError: convert(::Type{Union{Nothing, A, Int64}}, ::Int64) is ambiguous.

Candidates:
convert(::Type{Union{A, T}}, x::T) where T
@ Main REPL[4]:1
convert(::Type{T}, x) where T>:Nothing
@ Base some.jl:37
convert(::Type{Union{A, T}}, x) where T
@ Main REPL[3]:1
convert(::Type{T}, x::T) where T>:Nothing
@ Base some.jl:36
convert(::Type{T}, x::T) where T
@ Base Base.jl:84
To resolve the ambiguity, try making one of the methods more specific, or adding a new method more specific than any of the existing applicable methods.
``````

but without the above extensions of `Base.convert` it works fine:

``````julia> Union{Nothing, Union{A, Int}}[1][1]
1
``````

Is there a way to achieve this without producing bugs?

Can’t you achieve the same thing with constructors rather than introducing a general conversion rule?

``````julia> struct B3
x::Union{A, Int}
B3(a::A) = new(a)
B3(a) = new(convert(Int, a))
end

julia> B3(1)
B3(1)

julia> B3(true)
B3(1)
``````
1 Like

Seems like a very good idea, but I fail to see the generalization for my use case, for example

``````struct A end

struct B
x::Union{A, Int64}
y::Union{A, Int32}
z::Union{A, Int16}
k::Union{A, Int8}
end
``````

doesn’t this need a lot of constructors with your idea?

ok I have it maybe:

``````
julia> struct A end

julia> struct B
x::Union{A, Int64}
y::Union{A, Int32}
z::Union{A, Int16}
k::Union{A, Int8}
B(x, y, z, k) = new(maybe_convert(x), maybe_convert(y), maybe_convert(z), maybe_convert(k))
end

julia> maybe_convert(x::A) = x
maybe_convert (generic function with 1 method)

julia> maybe_convert(x::T) where {T} = convert(T, x)
maybe_convert (generic function with 2 methods)
``````
1 Like

I think you need to pass the type to `maybe_convert` or else it doesn’t do anything, no?

``````julia> struct A end
julia> struct B
x::Union{A, Int64}
y::Union{A, Int32}
z::Union{A, Int16}
k::Union{A, Int8}
B(x, y, z, k) = new(maybe_convert(Int64, x), maybe_convert(Int32, y), maybe_convert(Int16, z), maybe_convert(Int8, k))
end

julia> maybe_convert(_, x::A) = x
maybe_convert (generic function with 1 method)

julia> maybe_convert(T, x) = convert(T, x)
maybe_convert (generic function with 2 methods)
``````
1 Like

Lovely, thank you a lot!

I think you could even automate that… But not sure whether this code is a good idea in practice

``````function maybe_convert_recursively(T::Union, x)
try
return convert(T.a, x)
catch err
err isa MethodError && return maybe_convert_recursively(T.b, x)
rethrow(err)
end
end
maybe_convert_recursively(T, x) = convert(T, x)

struct B
x::Union{A, Int64}
y::Union{A, Int32}
z::Union{A, Int16}
k::Union{A, Int8}
function B(args...)
length(args) == fieldcount(B) || error("You need to pass \$(fieldcount(B)) arguments to construct a B. Got \$(length(args)).")
converted_args = map(maybe_convert_recursively, fieldtypes(B), args)
new(converted_args...)
end
end
``````
1 Like

mmmh, interesting, but this will be a bit slower I guess in some cases

I will use this inside a macro so I can actually create the syntax we discussed previously with a bit of effort

yeah, this is nice because it is recursive though, but I wouldn’t go this far, because I just want to mimick how normal structs work, I don’t think they have this kind of recursive behaviour

I actually don’t know whether this is slow or not… In principle it could compile away I think. And it actually does See here (my function is called `f` not `maybe_convert_recursively`)

``````julia> @code_native debuginfo=:none dump_module=:false (()->f(Union{A,B2,Int32,String}, "asdf"))()
.text
push	rbp
mov	rbp, rsp
mov	rax, qword ptr [r13 + 16]
movabs	rdi, 140250117903312
movabs	rsi, 140250096741664
mov	rax, qword ptr [rax + 16]
mov	rax, qword ptr [rax]
movabs	rax, offset f
call	rax
pop	rbp
ret
nop	word ptr cs:[rax + rax]
``````

No error handling to be seen
Edit: I actually take that back. I don’t know why I wrapped the function in an anonymous function but if you don’t do that then you clearly the error survive. More over if you simple profile the function it get clear, that it is in fact quite slow

``````julia> @btime f(Union{A,B2,Int32,String}, "asdf")
97.152 μs (40 allocations: 1.34 KiB)
"asdf"
``````

So don’t use this approach!

1 Like

When you hit the catch block it is usually slow in my experience, the try usually has small overhead instead