I am trying to write a generic constructor for a bunch of type aliases that all share a common structure:
# Abstract type AbstractA with a bunch of different concrete subtypes:
abstract type AbstractA end
struct A1{T} <: AbstractA
x::T
y::T
end
struct A2{T} <: AbstractA
x::T
end
struct A3{T} <: AbstractA
x::T
end
# Wrapper B with a corresponding bunch of aliases:
struct B{d, TA}
a::TA
B(d, a::AbstractA) = new{d, typeof(a)}(a)
end
const B1{d, T} = B{d, A1{T}}
const B2{d, T} = B{d, A2{T}}
const B3{d, T} = B{d, A3{T}}
But now i want to define constructors for the BX aliases, as
You’re trying to find a supertype of the aliases B1/B2/B3 for the method’s dispatch that also extracts A1/A2/A3 like type parameter values for B, but the aliases actually specified A1{T}/A2{T}/A3{T} substituted with a known T value as type parameter values for B. From another angle, we can’t make a supertype B{d, a{T}} where {d, a, T} or substitute a alone; a type parameter a is not a parametric type, even if we intend it to be, so it can’t have type parameters like T.
Thing is, you don’t specifically need a type parameter, you just need a UnionAll mapping Dict(B1=>A1,B2=>A2,B3=>A3). B1/B2/B3 aliases a parametric struct B, so it has the same sole field a::TA. You can use dump to check that B1/B2/B3 specified A1/A2/A3 as the sole TA, which is how it could adopt their parameter T. That’s why fieldtypes/fieldtype(b, :a) works perfectly.
Watch out for the type instability here. d being a value would make @code_warntype indicate abstract types, but d being a type would make @code_warntype deceptively assume a constant d value. B{d}(a::AbstractA) wouldn’t be out of line.
Thansk to both for all the explanations, this is clearer for me now.
This works perfectly.
Indeed, this is why i started reviewing this old code at the begining. I managed to adapt the previous case to provide the BX{d}(...) syntax :
function (b::Type{<:B{d}})(args...; kwargs...) where d
(ft,) = fieldtypes(b)
return B{d}(ft(args...; kwargs...)) # assuming this constructors exists and is defined in B struct.
end
But for compatibility reasons I need to keep the old unstable BX(d, args...) versions, and if I load both constructors i get an ambiguity…
Personally, I wouldnt’t bother if you do not call the constructors in performance critical code and just take the convenience. Having values as type parameters can be very convenient and I take the performance penalty in non performance critical code.
I am having the dilemma indeed. If I stay with the current situation, constructors are not type stable but then everything i do with the obejcts is, and i indeed do not construt them in performance-critical code. So we are good.
I just wanted to allow the construction as BX{d}(args...) as well, just in case. But I still cannot find the right syntax.
Did you try this out? It should recursively call itself and error.
I’m stil wondering if you need the first variadic argument args...? (I do not really use it, so there might be lack of knowledge on my side)
In the following code:
function (b::Type{<:B})(d, args...; kwargs...)
ft = fieldtype(b,1)
return B(d, ft(args...; kwargs...))
end
function (b::Type{<:B{d}})(args...; kwargs...) where d
ft = fieldtype(b,1)
return B(d, ft(args...; kwargs...))
end
b4 = B2{2}(rand(5))
I get an ambiguity since (b::Type{<:B}) is not more specific than (b::Type{<:B{d}}). And the args... allows an arbitrary amount of agruments (zero included) s.t. it cannot distinguish between (d, args...) and (args...) since args.. can just simply be empty.
However chaning args... to args works:
function (b::Type{<:B})(d, args; kwargs...)
ft = fieldtype(b,1)
return B(d, ft(args; kwargs...))
end
function (b::Type{<:B{d}})(args; kwargs...) where d
ft = fieldtype(b,1)
return B(d, ft(args; kwargs...))
end
b4 = B2{2}(rand(2))
b5 = B2(2,rand(2))
println(b4)
println(b5)
@YanickKind, Yes this is the ambiguity i hit. The solution with passing args as a vector is nice, but for the moment i need the variadic one indeed for retrocompatibility…
Copulas is a v0, so SemVer demands no version compatibility and Pkg only insists patches of a non-zero minor version be compatible. In other words, leading zeros indicate an experimental stage, and Julia shifts the breaking changes to the leading nonzero version as part of that. It wouldn’t be out of line for you to make a still-experimental v0.2 with refactored type structures and method tables. It’s far better to make such changes for v0, however insignificant, than v1 where there are more promises.
On the other hand, it looks like a medium to large package, and you mention compatibility with other packages that can be affected by refactors. It’s also not great to release a breaking version for just a few reasons because it’s very likely to discover other desired breaking changes that should have been included; people wouldn’t appreciate adjusting to v0.12.0 within a year. It makes sense not to jump the gun when the performance improvements aren’t certain.