Field specialization given parametric composite types

Hi there,

I would like to define a set of structs, which share their name (and thus type), but specialize their fields depending on their parametric type:

# abstract specialization types (of which there may be more than one parent type)
abstract type AbstractSpecializeParent end
# hierarchy structure of parent type
abstract type SpecializeTypeA <: AbstractSpecializeParent end
abstract type SpecializeTypeB <: AbstractSpecializeParent end

# struct of type ContainerType1 specialized to SpecializeTypeA
mutable struct ContainerType1{SpecializeTypeA}
	a
	b
	c
end
# struct of type ContainerType1 specialized to SpecializeTypeB
mutable struct ContainerType1{SpecializeTypeB}
	d
	e
end

which then allows me to do stuff like:

# a function which is specialized for ContainerType1 with subtype SpecializeTypeA
somefunction(obj::ContainerType1{T}) where {T<:SpecializeTypeA} = (obj.a + obj.b / obj.c,T)

As you see, each struct would have a different set of fields. However, for obvious reasons such definitions are not possible as that gives an Error: invalid redefinition of constant ContainerType1.
I want to do this anyway for several reasons:

  1. The fields depend on the parametric type,
  2. dispatch functions depending on the type and parametric type, and
  3. use the parametric type inside of the function.

I cannot use fields inside of the structs, which contain the specialization as symbols, because the parametric types have an hierarchy and I need to dispatch on that. To add further complexity: several “AbstractSpecializeParent” types and hierarchies exist in parallel and I need to dispatch on each of those individually. Thus, I cannot just do

mutable struct ContainerType1SpecializeTypeASpecializeTypeZ <: ?????
	specializeA::Symbol
	specializeZ::Symbol
	a
	b
	c
end

because it would need to inherit from several different specialization types!

I have found a workaround using a container struct to wrap the highly specialized concrete types. These concrete types now have these really ugly names, which I also wanted to avoid, but that’s just aesthetics:

# abstract super type of hidden structs
abstract type AbstractConcreteType1 end
# type specialized container for hiding structs
struct ContainerType1{T<:AbstractSpecializeParent}
	hiddenstruct::AbstractConcreteType1
end
# fields of the hidden concrete type act as fields of the super type (cannot access field hiddenstruct anymore)
Base.getproperty(obj::ContainerType1, sym::Symbol) = getproperty(getfield(obj, :hiddenstruct), sym)

# first hidden struct specialized for abstract type SpecializeTypeA
mutable struct ContainerType1SpecializeTypeA <:AbstractConcreteType1
	a
	b
	c
end
# quasi constructor with default field values
ContainerType1(::Type{T}) where {T<:SpecializeTypeA} = ContainerType1{T}(ContainerType1SpecializeTypeA(1,2,3))

# second hidden struct specialized for abstract type SpecializeTypeB
mutable struct ContainerType1SpecializeTypeB <:AbstractConcreteType1
	d
	e
end
# quasi constructor with default field values
ContainerType1(::Type{T}) where {T<:SpecializeTypeB} = ContainerType1{T}(ContainerType1SpecializeTypeB(4,5))

This allows me to very quickly and rapidly add following:

# sub type of specialization type
abstract type SpecializeTypeASub1 <: SpecializeTypeA end

# new conrete type
mutable struct ContainerType1SpecializeTypeASub1 <: AbstractConcreteType1
	a
	b
	c
	d
end
ContainerType1(::Type{T}) where {T<:SpecializeTypeASub1} = ContainerType1{T}(ContainerType1SpecializeTypeASub1(1,2,3,4))

Now the function somefunction() works exactly the same for this new subtype, however, where necessary, I can specialize functions:

somefunction(obj::ContainerType1{T}) where {T<:SpecializeTypeASub1} = (obj.a/obj.b - obj.c*obj.d,T)

The question I have is … should I even do this, or does this negatively impact performance?
I do realize that

struct ContainerType1{T<:AbstractSpecializeParent}
	hiddenstruct::AbstractConcreteType1
end

negatively impacts performance, because the compiler cannot generate specialized code, because the actual type of the field hiddenstruct is hidden.
I can work around that as well given

struct ContainerType1{S<:AbstractConcreteType1,T<:AbstractSpecializeParent}
	hiddenstruct::S
end

Then I need to adapt the quasi constructors and the function calls to incorporate the parametric type. (I have not yet gotten that to work.)

Besides the impact on performance, do you know of a better, less contrived approach I could use to realize this?

Thanks in advance!

PS: If any part of what I said does not make any sense to you, I am very happy to further explain. Additionally, if you think I structured my post badly in a particular way, please give me notice so I can improve it.

Maybe:

abstract type AbstractSpecializeParent end
# hierarchy structure of parent type
abstract type SpecializeTypeA <: AbstractSpecializeParent end
abstract type SpecializeTypeB <: AbstractSpecializeParent end

abstract type AContainerType{T} end


mutable struct ContainerType1 <: AContainerType{SpecializeTypeA}
	a
	b
	c
end

mutable struct ContainerType2 <: AContainerType{SpecializeTypeB}
	d
	e
end

somefunction(obj::AC) where AC<:AContainerType{T} where {T<:SpecializeTypeA} = (obj.a + obj.b / obj.c,T)

But do you really need to make it that complicated?

1 Like

Thank you for the suggestion. That’s much cleaner than my approach!

To answer your question: I don’t know exactly. The problem I try to solve incorporates a particular setup, where structs can be categorized according to two dimensions. The fields of these structs depend on those dimensions.

I hope this image clarifies the problem:

(There is even a third dimension, because different classfication schemes exit.)

Edit: What I forgot to mention … this is just the most straight forward description of the hierarchy dependencies I came up with. This does not mean it is the best or most efficient.