TIL: Inherited fields in structure subtypes are accessed with the accessor from the supertype

I guess the example is mostly to illustrate the behavior, but in this particular example my preferred way is to simply avoid using sub/super-types for this.

Use explicit conversion instead

So instead I would do this:

(struct pos2 (x y))
(struct pos3 (x y z))

(define (pos2->pos3 p)
  (match-define (pos2 x y) p)
  (pos3 x y 0))

This way you just deal with 2 separate simple types and convert between them explicitly where it is needed, I like that this makes things more explicit.

false simplicity, that leads to complicated code

Of course you could argue that subtypes are more convient because you can just treat a 3d-pos as a 2D pos, but I would argue that that isn't necessarily a good thing. Adding a (pos2 1 2) and (pos3 1 2 3) and getting (pos2 2 4) as result doesn't make that much sense.
Instead I would expect it to either error saying it doesn't implement adding to different types or return (pos3 2 4 3) which doesn't just silently drop the 3D-ness of one operand; and you don't get that without writing code that deals with the actual types. (don't make it more generic than it needs to be)

So I prefer when I am forced to explicitly write code that does what I want to happen, over code that just chooses one of the options that may or may not be appropriate.

I also think going from a 3D to a 2D there are 3 variants that make sense xy xz yz (ignoring one of the dimensions). So I don't think choosing one dimension to be auto converting while the others have to be converted through other explicit functions is good. Why?
Because when you keep things symmetrical (independent on what axis or plane you are focusing on), you end up being able to write code that is generic and fast. Instead of needing to special case based on what axis you are dealing with (e.g. sometimes you can use a macro to generate all the cases which means you type it once but it performs as if you had typed out all the repeating lowlevel permutations based on axis/plane).

Maybe there is even an argument to call pos2->pos3; pos2->xy-pos3 instead and have pos2->xz-pos3 and pos2->yz-pos3 too, (and then you could define arbitrary planes within the 3D space that contain the point) but I am getting too off topic...

Where do I use subtypes / or something else?

Apis / Interfaces / Runtime structure / organization

I think subtypes have their uses e.g. when you use them to create an api/interface that has different implementations that need their own specialized internal invariants/handles to resources etc., where you keep the generic stuff in the supertype and the specific in the subtype. (generics can be useful too)

I also want to mention that sometimes explicit composition by just having a struct member that is another struct might be a good choice.

I don't use classes often, but they don't leak their class name into every method name, so depending on what you are actually implementing they might be the natural choice for polymorphic things. (similar with generics)

Lowlevel

For things like positions and other lowlevel mathy stuff I want them to be as simple as possible with least amount of indirections because those are more likely to have a lot of instances and it is good when those can be easily transformed into a contiguous memory segment that is just pure data without pointers to other places. (Because then you can get good performance)
So I keep those as plain structs which are then transformed to buffers of data containing multiple positions one level more lowlevel.

I would avoid polymorphism for lowlevel stuff (and I prefer math to be lowlevel if possible)

Questions

This reminds of this post, because I feel like there are some questions here that could be asked:

What are good use cases for structs with super-/subtypes?
Where do you like to use them and how does it help you?

2 Likes