I proudly present Convertible.jl. The package provides the isconvertible trait that can be applied to type definitions via the @convertible macro. Types that share this trait can easily be converted into one another with a single call to Base.convert even though multiple intermediate conversions might be required.
For those of you who have seen my JuliaCon talk last year, this is a less dirty and generalised version of the type-system-exploiting conversion trick I presented there.
Example
Define convertible types:
@convertible struct A
val::Int
end
@convertible struct B
val::Int
end
@convertible struct C
val::Int
end
Convert… (you need to opt-in to this behaviour by wrapping the expression with @convert )
Internally Convertible.jl will compute the shortest conversion path and emit a specialized method based on a generated function, e.g. convert(C, convert(B, a)) in this case.
julia> a = A(1)
julia> @convert convert(C, a)
C(3)
I wonder if it would be possible for Convertible.jl to automatically define the missing convert methods instead of requiring the user to opt-in to the behavior.
I’m imagining a use case where I have ~10 different types that should all be convertible between each other and I don’t want to burden the user with using a macro to do the conversions (I’d like convert to just work). It would be neat if I could simply define a minimal number of convert methods and use Convertible.jl to define the missing ones.
The package originally worked this way (requiring just @convertible) but @tkelman asked me to change it. His objection was that changing the behaviour of a Base method upon import of a package isn’t very good style and might lead to subtle breakage in other people’s code. A valid point in my opinion.
In my packages I tend to hide the call to convert from the users anyways. The user is supposed to use type constructors instead. Like this:
abstract type Blob end
@convertible struct Foo <: Blob end
@convertible struct Bar <: Blob end
@convertible struct FooBar <: Blob end
convert(::Type{Bar}, ::Foo) = Bar()
convert(::Type{FooBar}, ::Bar) = FooBar()
(::Type{T}){T<:Blob, S<:Blob}(s::S) = @convert convert(T, s)
@convertible struct A end
@convertible struct B end
@convertible struct C end
convert(::Type{B}, ::A) = B()
convert(::Type{C}, ::B) = C()
@define_convert_method A C
# expands to: convert(::Type{C}, x::A) = convert(C, convert(B, x))
I think if you do it this way you’re not changing the behavior of convert when Convertible.jl is imported. Rather it’s just helping you define a bunch of additional methods for convert.
I think I would also have something like @generate_convert without arguments that generates the missing methods for all permutations if you have many convertible types.
Your approach shifts the costs from run-time to compile-time which might be good for many applications.