Write a constructor for these aliases?

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

BX(d, args..., kargs...) = B(d, AX(args...; kwargs...))

for all AX<:AbstractA and associated BX.

I tried:

(::Type{<:B{D, <:AbstractA} where D})(d, args...; kwargs...) = "Dispatches but dont know which A here."

Also :

(::Type{<:B{D, <:A} where D})(d, args...; kwargs...) where A = "This one wont work, but should gives access to A"

An idea on the right way to write this constructor ?

1 Like

Did I understand that correctly that you want to to define constructors for all B’s (B1,B2, B3) within one method?

In case of yes, this works for me:

function (b::Type{<:B})(d, args...; kwargs...) 
    (ft,) = fieldtypes(b)
    return B(d, ft(args...; kwargs...))
end 


b1 = B1(1 , "GrĂĽĂźe", "Test")
println(b1)
b2 = B2(2,2)
println(b2)
b3 = B3(3,10.)
println(b3)

yields

B1{1, String}(A1{String}("GrĂĽĂźe", "Test"))
B2{2, Int64}(A2{Int64}(2))
B3{3, Float64}(A3{Float64}(10.0))

Edit: you can simply use ft = fieldtype(b,1) instead of (ft,) = fieldtypes(b)

3 Likes

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.

2 Likes

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…

1 Like

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)

yields

B2{2, Vector{Float64}}(A2{Vector{Float64}}([0.047862735617204555, 0.38961447120103077]))
B2{2, Vector{Float64}}(A2{Vector{Float64}}([0.751595176865353, 0.4652809371308122]))

Is there a case where you need the variadic argument?

Edit: Code formatting, typo

@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…

BTW, Cross-link to the concrete issue in the package: [Constructors] Propose type-stable constructors · Issue #333 · lrnv/Copulas.jl · GitHub

Ah ok, perhaps someone more experienced might help. Sorry I cannot be more of a help.

Thanks anyway, i learned a lot already :wink:

1 Like

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.

2 Likes

@Benny What you say is very wise and largely applicable to other packages as well. I will definitely follow these advices.