JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.43k stars 5.45k forks source link

abstract types with fields #4935

Open JeffBezanson opened 10 years ago

JeffBezanson commented 10 years ago

This would look something like

abstract Foo with
    x::Int
    y::String
end

which will cause every subtype of Foo to begin with those fields.

Some parts of the language internals already anticipate this; it's a matter of hooking up the syntax and filling in a few missing pieces.

zot commented 2 years ago

One of the main issues I had with this feature was that when you add a field to an abstract super type you then cannot remove it. Which is especially unfortunate since #24960 because it's entirely possible that you might write generic code for an abstract type that accesses a field that isn't even there for some specific implementation. So now you find yourself in a situation where you want a way to delete a field in a subtype that was inherited from an abstract supertype. So now we need two language features. It seems far simpler to just put the .core field in all the implementations of a subtype — except, of course, for the ones that don't end up needing it.

Do structural interfaces help with this by potentially removing the need for inheritance?

henriquebecker91 commented 2 years ago

I really hope this feature never gets implemented. Removing fields from abstract structures was one of the best design decisions of Julia.

wsshin commented 2 years ago

@henriquebecker91, could you elaborate? I'm curious about the advantages of removing fields from abstract structures.

martinholters commented 2 years ago

To throw a new thought into the discussion: Inheriting fields is subclassing, while Julia focuses on subtyping. In C++, subclassing and subtyping are mashed together, so the distinction may be non-obvious to many. But it might be interesting to consider what would happen if Julia gained subclassing as a distinct concept... (To make the distinction obvious: every a square is a rectangle, so Square <: Rectangle would make sense, but subclassing would work better the other way round: Rectangle could inherit the single property width from Square and extend with a new property height.)

opiateblush commented 2 years ago

I find it very curious that such a feature is still up for debate after almost a decade. The more I read through the discussions about inheritance in Julia the more I feel back at school where people were complaining about their favorite music artist going "mainstream". As if it was important to preserve a unique feature just because of its uniqueness. I don't see any significant disadvantages by allowing inheritance of structure as it proposed in this issue. Here is why I think this is a good thing to have in Julia:

To be honest, I don't quite get the gist of abstract types with their current implementation. In the docs it says it's about inheritance of behavior. But the way I see it, there is nothing to inherit from. If you define an abstract type, the interfaces for it must be defined in the docs and cannot be enforced. Structure is irrelevant at this point, fine. So, now you can dispatch on that abstract type assuming that the actual implementation supports the interface defined in the docs. Great, but the actual implementation doesn't have to implement anything, it just throws an exception at runtime if it doesn't. Furthermore, if it was only about behavior, what about a different type that implements the interface but is not a concrete subtype of the abstract type? Well, if I wanted my function to work with any type that supports a specific behavior I couldn't dispatch on any type. What are abstract types there for then?

Don't get me wrong. I don't want to offend anyone and I might be a bit emotional right now, but I still find my points valid. I also find Julia a great language with huge potential to eliminate strange constructs of Python and C/C++ code (and similar) just to benefit from both their advantages. With Julia I can switch between the flexibility of Python, the speed C or anything in between just as I go. It's awesome!

zot commented 2 years ago

To be honest, I don't quite get the gist of abstract types with their current implementation. In the docs it says it's about inheritance of behavior. But the way I see it, there is nothing to inherit from. If you define an abstract type, the interfaces for it must be defined in the docs and cannot be enforced. Structure is irrelevant at this point, fine. So, now you can dispatch on that abstract type assuming that the actual implementation supports the interface defined in the docs. Great, but the actual implementation doesn't have to implement anything, it just throws an exception at runtime if it doesn't. Furthermore, if it was only about behavior, what about a different type that implements the interface but is not a concrete subtype of the abstract type? Well, if I wanted my function to work with any type that supports a specific behavior I couldn't dispatch on any type. What are abstract types there for then?

If you need to handle access in a general way, you can define getter methods (ala OOP) to access the parts you need and use them in the more general methods, like:

showquadrangle(q::Quadrangle) = println("$(nameof(typeof(q)))[$(width(q)) x $(length(q))]")
width(s::Square) = s.size
length(s::Square) = s.size
width(r::Rectangle) = r.width
length(r::Rectangle) = r.length

This is reminiscent of Smalltalk where you can't directly access instance variables in another object without using reflection, you have to use accessor methods for that.

Deduction42 commented 1 year ago

I'm just getting bit by this problem too. I have very generic types of equipment that require a bare minimum set of fields for functions to work, and I'm specializing them so that they always have the same fields as the parent. If I was able to define a set of fields for an abstract type that must be available to all sub-types, it would make my code adhere better to DRY.

I'm aware from https://github.com/JuliaLang/julia/pull/24960 that getproperty can be used to overload property access, but if someone Author A defined a required field for an AbstractType but user B wants to subclass it in a way for that field to not be there, user B could simply

  1. Build their own constructor that fills that property with a default value
  2. Override that property access with the desired behaviour in "getproperty" higher up in the "if statement" block

Even if this hack were undesirable, I don't think it would be too hard to have a "@deletefield" macro to delete "inherited" fields from abstract types. I already wrote a macro that includes fields from another type and can even omit a set of fields if so desired. Field removal from an abstract parent should be quite rare, and the only need I'd see from it would be if there was a "lazy" way to fill that field with an inferred value on the fly via a getproperty branch.

From this, fields on Abstract Types should therefore be a sort of interface spec. If there is a field on an Abstract Type, then getproperty should be defined for it. Period. That way, any function that uses "getproperty" can be guaranteed that those properties exist. That's just how interfaces work. It's certainly less work and less error-prone than copy-pasting the same fields over and over again or re-implementing a complete set of constructors for every subtype.

If it turns out that abstract types with fields is a bad idea, would it be acceptable to have a macro that expands getproperty and propertynames that include the properties of a nested object?

jkosata commented 10 months ago

While this is under debate (maybe another decade :)) ), I hacked this together to bypass the issue, DefaultFields.jl It lets you define supertype along with a macro, calling which then generates new types with the required fields added:

@with_fields abs_type a::Int b::Float64
@abs_type struct mystruct
            c::String
        end

julia> fieldnames(mystruct)
(:c, :b, :a)
julia> fieldtypes(mystruct)
(String, Float64, Int64)

Hope it helps!