Why is it impossible to subtype a struct?

There’s a lot of good ideas in this thread, but as far as I can tell they’ve been discussed in more detail in the github issue linked above by @Tamas_Papp, have you read it? Especially the (for me) 127 hidden discussion elements hidden by github are insightful as to how this might look in the future.

There’s also a very long discussion about how to handle those kind of relationships between Rectangle and Square you are talking about and how to solve that problem.

It’s a very insightful (albeit long) read, if I manage to make some time later today I can maybe summarise what I’ve understood from it.

2 Likes

I like this approach. Only use field access where you really have to, and then define higher level methods on the abstract supertype only:

abstract type AbstractRectangle{T} end

struct Rectangle{T} <: AbstractRectangle{T}
    width::T
    height::T
end
width(x::Rectangle) = x.width
height(x::Rectangle) = x.height

struct Square{T} <: AbstractRectangle{T}
    side::T
end
width(x::Square) = x.side
height(x::Square) = x.side

# Now define methods on abstract supertype
area(x::AbstractRectangle) = width(x) * height(x)
shortside(x::AbstractRectangle) = min(width(x), height(x))
longside(x::AbstractRectangle) = max(width(x), height(x))
circumference(x::AbstractRectangle) = 2 * (width(x) + height(x))
diagonal(x::AbstractRectangle) = sqrt(width(x)^2 + height(x)^2)
# etc. etc.

Whenever you need to optimize for performance, you can overload particular methods:

diagonal(s::Square) = sqrt(2) * width(s)
7 Likes

The corollary is “never use field access outside the module which defines the composite type”.

For a given value of never :wink:

3 Likes

Yes. And even inside the module, I try to use field access as little as possible, just defining a few very primitive functions that do field access, and building everything on top of those. Perhaps even to a slightly excessive degree…

2 Likes

I don’t think this is excessive, it is just good habits.

The only thing preventing me from doing it more is that occasionally I ponder over the merits of naming the accessor thing vs getthing vs get_thingy etc for 10 minutes, which is how I know it is time for a coffee break :wink:

4 Likes

If this was possible then you could never store such a type inline, and this is a cost that every type would pay, not just the ones that are actually subtyped. That would severely undermine Julia’s quality and utility for efficient (numerical) computing. In order to avoid this cost, you’d have to get into OOP modifier games with final types or since we’d probably want that to be the default, some kind of nonfinal keyword (ugh), which seems super distasteful. I think having better support for copying a type’s structure and/or methods with modifications seems like a better direction.

In terms of the Rectangle example, I don’t really see why this arrangement is so bad:

abstract type AbstractRectangle{T<:Real} end

struct Rectangle{T<:Real} <: AbstractRectangle{T}
    width::T 
    height::T 
end

struct Square{T<:Real} <: AbstractRectangle{T}
    side::T 
end 

Is the objection that you need to have an extra abstract type? That seems like a significant conceptual clarification to me. You can even define square.width and square.height if you want to.

7 Likes

Out of curiosity, is it also idiomatic to define a formal method for the abstract type with no contents so that the reader (of the code) can see what methods are to be defined for concrete types (i.e. for documentation/readability purpose)?

abstract type AbstractRectangle{T} end

width( x::AbstractRectangle ) =
  error("width() should be implemented for ", typeof(x))
2 Likes

Not sure. I don’t think it’s common, but it doesn’t sound like a terrible idea. It will anyway show up as a missing method error, though, so it’s perhaps a bit redundant.

1 Like

For this, that feature would be nice: https://github.com/JuliaLang/julia/issues/7512, https://github.com/JuliaLang/julia/pull/24299.

1 Like

It is usually done to document the generic function, eg

"""
    foo(bar, ...)
"""
function foo end

IMO the error message is usually redundant — if you want to do it anyway, throw a MethodError.

2 Likes