Struct as subtype of multiple types

I am trying to create 2 groups of abstract types to be for different purposes. A quick example can be an abstract type GeometricEntity of which both AbstractPoint (supertype for n-dimensional points) and GeometricEntity2D (intended to be supertype for 2D vectors and 2D points) are subtypes. Now I get an error on defining a concrete type Point like this:

struct Point2{T} <: Union{GeometricEntity2D,AbstractPoint}
    x::T
    y::T
end

There is a workaround, but not a neat (and extendible) one, by having GeometricEntity2D as a type alias of the union of all 2D geometric entities as follows:

const GeometricEntity2D = Union{Point2,Vector2}

Besides stylistic problems, this definition has to go after the definition of all concrete “supertypes” and will have to be kept track of manually. Is there a way to properly achieve GeometricEntity2D being a supertype, preferably abstract?

First of all, welcome to our community, :confetti_ball:

Second, are you aware that you can have parametrized abstract types? So you do not need to manually create a different type for each number of dimensions?

Second, it seems like a case for the Holy Traits pattern.

Third, why you need these abstract types? You will dispatch on them? Note that you have the following problem: if you have a function f that has a fallback method for the abstract GeometricEntity2D and another for the AbstractPoint but no method for the concrete Point2{T} then which of these two fallback functions should be called? I would suggest considering the following alternative: you can simply create a lot of fallback functions inside modules to simulate “interfaces”, i.e., module GeometricEntity2D has all basic functions that a GeometricEntity2D should implement, i.e., any types that are particular cases of that concept should extend these functions for their concrete type. And then your functions that use GeometricEntity2D types to do more advanced stuff will take generic parameters (not even restricting toGeometricEntity2D) and just call the function the concrete type should have implemented (and if it failed to do so, then an error will be thrown).

6 Likes

In short, this is requesting https://github.com/JuliaLang/julia/issues/5! With multiple dispatch, it is simply nontrivial to define the behavior, so it has not yet been attempted to be defined. Though I think the solutions given as suggestions (particularly Holy Traits) may help guide you to an alternative design.

3 Likes

Hi, thanks for the warm welcome! :smiley:

Yes, probably parametric types are what will be scalable; I will skim through the design patterns in the book you shared too, and if you have any recommendations on what pattern best suits this, please do let me know so that I can focus on that first. And yeah, I need abstract types for dispatch. There could be plots involved for particular groups and constructors involving another, and all of these groups will have a non-empty intersection.

Creating modules/interfaces for this also seems like a good design approach. Can you elaborate on this a bit more, please! Still pretty new to the language, so it will be nice to have some quick prototypes to follow. I can provide more details:

There are Points (n coordinates and reference frame), Frames (n coordinates and theta wrt world origin), Poses (base frame and a head frame). I need to be able to plot frames and points and do algebra on all 3 types too.

Thanks! I have started going through it :slight_smile:

Also, you might find those types are already well implemented: https://github.com/JuliaGeometry/GeometryBasics.jl

Using these would make your project compatible with a bunch of other Julia packages.

2 Likes

True yeah, I’ll consider extending the Abstract types in this package. Thank you!

Sorry, what you mean by that is not very clear to me.

What I mean by interfaces is something like this (note all the code above is merely an example, not real code):

module Plottable # this is the interface
    function plot end
end

function Plottable.plot(x :: Point, canvas :: Canvas)
end

function Plottable.plot(x :: Frame, canvas :: Canvas)
end

function plot_in_pdf(x, filename)
    # create a canvas object
    Plottable.plot(x, canvas)
    # output the plot to the filename
end

Basically, define a set of functions inside a module (Plottable in this case). And then you extend that functions for your concrete types (Point, Frame, and so on, I left the type parameters out for simplicity). Finally, you define a generic function which does not specify the types of the parameters implementing Plottable and just call the interface functions (the only one is Plottable.plot in this example) over such generic parameters. This is extensible: you can have as many different interfaces you want, and each concrete type may implement as many interfaces you want them to implement. One drawback is that this strategy alone does not deal with multiple fallback methods. This is, for each function of the interface: you will have one method for each concrete type, and maybe you will have one fallback method for Any (which may error and point out that you should implement the interface, or try to execute calling other more primitive functions of the same interface). If you start to define fallbacks (or even generic functions that should work over the interface) but restrict the type of the parameters to GeometricEntity2D, or any other abstract types, then you will start having the same problem again, because at some point you will want the same concrete type to inherit two different abstract types so it has access to all the functions you want it to have access. The solution I delineated here, therefore, is not restricting access: accept any type and expect it to implement the necessary interface.

1 Like

Nice, this makes sense! Thanks a lot :smiley: