Parametric type of another parametric type

TL;DR: how to define a parametric type where the parameter is another parametric type?

I want to design some algorithms handling point clouds, where each point has some weight. I want to allow for general point clouds as well as point clouds with some structure (say, living on a grid), so I define the abstract type

abstract type AbstractPointCloud{D} end

Then, a struct holding a generic point cloud could be defined as

mutable struct PointCloud{D} <: AbstractPointCloud{D} 
    weights::Vector{Float64}
    positions::Matrix{Float64}
    
    function PointCloud(weights, positions)
        D = size(positions, 1)
        length(weights) == size(positions, 2) || error("lenghts of weights and positions not matching")
        new{D}(weights, positions)
    end
end

where weights[i] encodes the weight of point positions[:,i].

So far everything works. But now I want to define another struct that encodes a given point cloud at different resolutions. This could be just a list of the point clouds at those different resolutions, but assume I want to store more info, like for example how to refine one point cloud to another. Then my guess of how to do it is:

mutable struct MultiScalePointCloud{C<:AbstractPointCloud{D}} 
    clouds::Vector{C}
    # plus other info about the point clouds
end

But this throws an error:

UndefVarError: D not defined

Stacktrace:
 [1] top-level scope
   @ In[3]:1
 [2] eval
   @ ./boot.jl:360 [inlined]
 [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
   @ Base ./loading.jl:1094

If I try to leave D a free parameter:

mutable struct MultiScalePointCloud{C<:AbstractPointCloud{D}} where D
    clouds::Vector{C}
    # plus other info about the point clouds
end

I get another error:

syntax: invalid type signature around In[4]:1

Stacktrace:
 [1] top-level scope
   @ In[4]:1
 [2] eval
   @ ./boot.jl:360 [inlined]
 [3] include_string(mapexpr::typeof(REPL.softscope), mod::Module, code::String, filename::String)
   @ Base ./loading.jl:1094

So, how would one overcome this? Thanks!

what about:

mutable struct MultiScalePointCloud{C<:AbstractPointCloud{D} where D}
    clouds::Vector{C}
    # plus other info about the point clouds
end

or I believe you could avoid D at all as well

1 Like

Ugh. Embarrassing how simple it was! Thanks a lot! :slight_smile:

1 Like

But now, would it be possible to make MultiScalePointCloud{D} also a subtype of AbstractPointCloud{D}? The following trials don’t work for me:

mutable struct MultiScalePointCloud{C<:AbstractPointCloud{D} where D} <: AbstractPointCloud{D}
    clouds::Vector{C}
    # plus other info about the point clouds
end

or

mutable struct MultiScalePointCloud{C<:AbstractPointCloud{D}} <: AbstractPointCloud{D} where D
    clouds::Vector{C}
    # plus other info about the point clouds
end

Maybe I forgot to add that D would encode the dimensionality of the ambient space of the point clouds, so it would be the same for both.

Maybe something like:

abstract type AbstractPointCloud{D} end

mutable struct PointCloud{D} <: AbstractPointCloud{D} 
    weights::Vector{Float64}
    positions::Matrix{Float64}
    
    function PointCloud(weights, positions)
        D = size(positions, 1)
        length(weights) == size(positions, 2) || error("lenghts of weights and positions not matching")
        new{D}(weights, positions)
    end
end

mutable struct MultiScalePointCloud{D} <: AbstractPointCloud{D}
    clouds::Vector{<:AbstractPointCloud{D}}
    # add constructor here to compute D with the given `clouds` and perform any needed check
end

Nothe the difference with:

mutable struct MultiScalePointCloud2{D} <: AbstractPointCloud{D}
    clouds::Vector{AbstractPointCloud{D}
end

which I believe will be type unstable, given that:

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
mpc1 = MultiScalePointCloud{2}([pc1, pc2])
mpc2 = MultiScalePointCloud2{2}([pc1, pc2])

yields to:

julia> typeof(mpc1.clouds)
Vector{PointCloud{2}} (alias for Array{PointCloud{2}, 1})

julia> typeof(mpc2.clouds)
Vector{AbstractPointCloud{2}} (alias for Array{AbstractPointCloud, 1})

I thought about this really fast so there might be some errors, but I hope this helps!

did it work?

Hey, first off thanks for all the help. I think that your second proposal is type-unsable, because MultiScalePointCloud doesn’t carry the information about the type of point clouds that it holds. Of course as soon as one extracts the point clouds from the vector one knows the correct type, so maybe it isn’t so bad for my use case, I still don’t know… I think that I would go for your first solution: having a type-stable struct even even though it isn’t a subtype of AbstractPointCloud. Still, if we eventually find the syntax to make the subtyping work that would be optimal :slight_smile:

Small update, the following works (though the parametric type info seems redundant and a bit ugly):

mutable struct MultiScalePointCloud3{C<:AbstractPointCloud{D} where D, D} <: AbstractPointCloud{D}
    clouds::Vector{C}
    
    function MultiScalePointCloud3(clouds::Vector{<:AbstractPointCloud{D}}) where D
        new{eltype(clouds), D}(clouds)
    end        
end

Then running your example works:

julia> pc1 = PointCloud(rand(2), rand(2, 2)); pc2 = PointCloud(rand(2), rand(2, 2)); 
julia> MultiScalePointCloud3([pc1, pc2])

MultiScalePointCloud3{PointCloud{2}, 2}(PointCloud{2}[PointCloud{2}([0.35587458766228375, 0.6051753465967098], [0.4138969988482093 0.6551431546310509; 0.14082576707455363 0.201651648972335]), PointCloud{2}([0.625358369477327, 0.44195778548245435], [0.016822896118342312 0.7482201544433038; 0.9469228843657014 0.09926806881414962])])

julia> typeof(PC)<:AbstractPointCloud{2}
true

yes, my second proposal is type unstable (I wanted to highlight that). But the first proposal is not! I mean:

mutable struct MultiScalePointCloud{D} <: AbstractPointCloud{D}
    clouds::Vector{<:AbstractPointCloud{D}}
    # add constructor here to compute D with the given `clouds` and perform any needed check
end

is not type stable.

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
mpc = MultiScalePointCloud{2}([pc1, pc2])

then:

julia> mpc
MultiScalePointCloud{2}(PointCloud{2}[PointCloud{2}([0.5813581709551043, 0.140386806689498], [0.7223741804200698 0.8801602352919129; 0.13548673801564037 0.04377817825836949]), PointCloud{2}([0.8702085871843692, 0.001544652640629307], [0.6039037966968885 0.21263956637047965; 0.49194113259410543 0.746045152172701])])

julia> typeof(mpc1) <: AbstractPointCloud{2}
true

julia> typeof(mpc.clouds)
Vector{PointCloud{2}} (alias for Array{PointCloud{2}, 1})

If you want to add the type, just add a new parameter:

mutable struct MultiScalePointCloud{D,T} <: AbstractPointCloud{D}
    clouds::Vector{<:AbstractPointCloud{D}}
    # add constructor here to compute D and T with the given `clouds` and perform any needed check
end

and compute T in the constructor. Also you could do the following:

mutable struct MultiScalePointCloud{D,T} <: AbstractPointCloud{D,T}
    clouds::Vector{<:AbstractPointCloud{D,T}}
    # add constructor here to compute D and T with the given `clouds` and perform any needed check
end

The complete example would then be:

abstract type AbstractPointCloud{D,T} end

mutable struct PointCloud{D,T} <: AbstractPointCloud{D,T} 
    weights::Vector{T}
    positions::Matrix{T}
    
    function PointCloud(weights::Vector{T}, positions::Matrix{T}) where {T}
        D = size(positions, 1)
        length(weights) == size(positions, 2) || error("lenghts of weights and positions not matching")
        new{D,T}(weights, positions)
    end
end

mutable struct MultiScalePointCloud{D,T} <: AbstractPointCloud{D,T}
    clouds::Vector{<:AbstractPointCloud{D,T}}
    # add constructor here to compute D and T with the given `clouds` and perform any needed check
end

this might help a little bit more:

abstract type AbstractPointCloud{D,T} end

mutable struct PointCloud{D,T} <: AbstractPointCloud{D,T} 
    weights::Vector{T}
    positions::Matrix{T}
    
    function PointCloud(weights::Vector{T}, positions::Matrix{T}) where {T}
        D = size(positions, 1)
        length(weights) == size(positions, 2) || error("lenghts of weights and positions not matching")
        new{D,T}(weights, positions)
    end
end

import Base: eltype, length

length(::PointCloud{D}) where {D} = D
eltype(::PointCloud{D,T}) where {D,T} = T

mutable struct MultiScalePointCloud{D,T} <: AbstractPointCloud{D,T}
    clouds::Vector{<:AbstractPointCloud{D,T}}
    function MultiScalePointCloud(clouds::Vector{<:AbstractPointCloud})
        D = length(first(clouds))
        if !all(cloud -> isequal(length(cloud), D), clouds)
            return error("lenghts must match")
        end
        T = promote_type(eltype.(clouds)...) # should they match?
        return new{D,T}(clouds)
    end 
end

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
mpc = MultiScalePointCloud([pc1, pc2])

Thanks a lot for all the details! Unfortunately, I still think the implementation is type unstable. A function that takes PC of type MultiScalePointCloud{D} can’t know what is the type of the vector PC.clouds: working as in your example,

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
PC = MultiScalePointCloud([pc1, pc2])

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
PC = MultiScalePointCloud([pc1, pc2])

check_point_cloud_type(PC) = typeof(PC.clouds)
@code_warntype check_point_cloud_type(PC)

shows type-unstabilities. Avoiding this was my motivation to have the eltype of PC.clouds as a parameter of MultiScalePointCloud. That is achieved by your original answer (at the cost of MultiScalePointCloud not being a subtype of AbstractPointCloud) and by my answer at Parametric type of another parametric type - #8 by ismedina (at the cost of being cumbersome to read). And I am starting to doubts that one can get the best of both worlds… But so far this was a very instructive exchange :slight_smile:

I see! here is the type stable case:

abstract type AbstractPointCloud{D,T} end

mutable struct PointCloud{D,T} <: AbstractPointCloud{D,T} 
    weights::Vector{T}
    positions::Matrix{T}
    
    function PointCloud(weights::Vector{T}, positions::Matrix{T}) where {T}
        D = size(positions, 1)
        length(weights) == size(positions, 2) || error("lenghts of weights and positions not matching")
        new{D,T}(weights, positions)
    end
end

import Base: eltype, length

length(::PointCloud{D}) where {D} = D
eltype(::PointCloud{D,T}) where {D,T} = T

mutable struct MultiScalePointCloud{D,T,C<:AbstractPointCloud{D,T}} <: AbstractPointCloud{D,T}
    clouds::Vector{C}
    function MultiScalePointCloud(clouds::Vector{C}) where {C<:AbstractPointCloud}

        # TODO: extend this function by checking `C` fulfills all the conditions you need

        D = length(first(clouds))
        if !all(cloud -> isequal(length(cloud), D), clouds)
            return error("lenghts must match")
        end
        T = promote_type(eltype.(clouds)...) # should they match?
        return new{D,T,C}(clouds)
    end 
end

pc1 = PointCloud(rand(2), rand(2, 2))
pc2 = PointCloud(rand(2), rand(2, 2))
mpc = MultiScalePointCloud([pc1, pc2])