Open JeffBezanson opened 10 years ago
My view is that inheriting from concrete types is illogical. This fact then manifests itself as problems in various flavors depending on the language. Definitely the specific problem described in the c++ article does not affect Julia, but I thought it might be useful to provide a broader context for the question. The article is an excerpt from Scott Meyers "More Effective C++". In my opinion, it makes sense to subtype concrete types only if the subtype specifies a subset of the already given possible values of the type being subtyped. Subtyping Int64 to get the type of e.g. even numbers would be OK. I am for abstract types with fields and interfaces.
I think many problems of various languages related to inheritance are due to not separating "conceptual" and implementation inheritance well enough. In this respect, Julia is in my opinion very well designed. Nevertheless, things like multiple abstract inheritance and interface specifications (of which fields in abstract types are a special case) would make the design more "complete".
@pbazant but would you consider the fields to be part of a public or private interface?
My take is that multiple abstract inheritance and public interface specifications are the most important issues to be solved in this area. Private interface specifications (which this issue is IMHO about) are neat but also make the language more complex.
@pbazant For Julia I'll agree with you (I think subclass from concretes works okay for languages like C++ and Python).
I agree with your other statements.
@tknopp I might be wrong, but it seems like the private/public debate is a separate issue. You could assume for now everything is public without breaking anything.
AFAIK, at the moment type-fields are considered private and the function-interface is public. For instance, it's length(Dict(...))
and not Dict(...).count
.
I don't think that the private/public debate is a separate issue. This is the key issue when we talk about overloading .
Note that this of course only applies to mutable types. For immutables, the fields are the public interface.
I can see where it is an issue for overloading the .
operator, but is it also an issue for abstract types with fields?
There needs to be a separation between interface and implementation. ATM they are the same thing, so any inheritance (meaning is-a) also would inherit the fields of the abstract type, an implementation artifact. But the derived type may wish to provide a completely different implementation. So the two things need to be separated. That is where most languages fall down and Julia has the chance to get it right.
@elextr You are absolutely right that interface and implementation have to be separated. If we say that only functions are the interface (e.g. length
, size
, ... of an AbstractArray) then the fields are implementation details. The abstract types with fields would however not really hurt this thinking. They would just be a convenient feature in some very OO-like situations. AbstractArray would not be such a thing. Personally I would avoid situations with deep inheritance hierarchies where abstract types with fields (heck we need an abbreviation for it: ATWF) are really needed. But here we get into a subjective area and if @abeschneider likes this programming style I don't really see the issue with this feature.
I think the following example is worth discussing: We have an Array
type and want to extent it with physical dimensions:
Solution a): Introduce a new PhysicalArray
type that has as field an ordinary Array
and a tuple of spacings. Problem: We have to forward all the methods.
Solution b): Let the entire Array
implementation "open" and define all the methods for the abstract type with fields AlmostConcreteArray
. Then a trival concretization gives us our Array
type. PhysicalArray
could now be derived from AlmostConcreteArray
in the same way (AlmostConcretePhysicalArray
with trivial concretization PhysicalArray
.
While I am personally more a fan of solution a) I can absolutely understand that people also like b). It would certainly make the language a little larger. But I am not sure if this is such an issue at this front where it is hard to force people to use a certain programming style.
@tknopp, (a) seems like a more flexible approach, and with a @delegate
helper macro (#3292) it need not be so difficult as it is now.
@tknopp I think issue #7442 shows a fair example of wanting abstract types with fields without having a deep hierarchy. So I would claim this issue isn't just about trying to create deep hierarchies (which for the record, I am usually against). Besides grouping (which requires a lot of boiler-plate functions) or copying of code (which violates DRY) I don't see a good way to implement those types.
@stevengj I am absolutely in line with you. I am just not sure if this has a clear better / worse answer or if this is about taste. @abeschneider is apparently of a different opinion and I was not able to convince him that (a) is a clean solution.
I think how well (a) works depends on the number of fields defined. In #7442 I tried both copying the variables and forwarding. Even with a small number of variables both involve writing a lot of extra code -- imagine the non-simplified version with many more fields.
Abe, I know that we are going full circle now, but for completeness here is the version that uses the has-a
relation.
abstract Player
type PlayerFields
position::Array{Float64, 1}
rotation::Float64
end
type PlayerA <: Player
base::PlayerFields
name::String
PlayerA() = new( PlayerFields([0.0, 0.0], pi), "foo" )
end
type PlayerB <: Player
base::PlayerFields
PlayerB(pos::Array{Float64, 1}, rot::Float64) = new(PlayerFields(pos, rot))
end
move!(player::Player, velocity:Array{Float64, 1}, dt::Float64)
player.base.position += velocity*dt
end
a = PlayerA()
b = PlayerB([10.0, 10.0], -pi)
move!(a, [0.1, -0.1], 0.1)
move!(b, [0.1, -0.1], 0.1)
You might not like it but it seems that many Julia developers got used to this. Typically the PlayerFields
type is not so artifical but stands on its own.
Just one other side note. In C++ inheriting from stl-containers is not encouraged. This has of course again specific C++ reasons but it is yet another example where has-a
is favored.
@tknopp You can do that (and I think that's also in #7442), but then you have to know that every player has a field named base
. This seems like something a compiler should enforce.
You can get around that problem (also in #7442) by defining an interface with methods, but then you have a ton of extra code that you have to write.
Both approach seem sub-optimal to me and a place where the compiler can help out. This could potentially provide better error messages, opportunities at optimization, and cleaner code.
The thing with c++ is it that it tries very hard to be paradigm free. There are specific cases where you don't want inheritance. But it's no uncommon to employ inheritance with templates (especially template hacking). Most of the time you only ever want to use single inheritance, except for those times you really need multiple inheritance. I think that's what makes it such as successful language -- it tries not to impose its ideals on you (some Java did entirely incorrectly, IMHO).
This seems like something a compiler should enforce.
We generally take the approach that features that serve to constrain what the programmer can do are unimportant, while features that allow programmers to do more things more easily are very important. Arguing for features so that the compiler can prevent people from doing things is unlikely to be particularly persuasive around here – that's just not the Julian attitude.
@StefanKarpinski I think this is very much the case of helping the user out, and I probably should not have used the word 'enforce'. I don't see how adding fields to abstract types would in any way constraint the user.
Right, the feature part doesn't at all constrain the user. But, if the main point of the feature is to tell the programmer when they got something wrong, then that's the limitation thing again. I was all for this until I realized that all it saved was a bit of typing and means that a subtype of an abstract type with fields cannot not have those fields – and it's entirely plausible that a subtype might decide that it wants to store the fields that the supertype declared in a different way, such as part of the type or computed from other fields. This comes down to inheriting structure versus behavior. Inheriting structure is restrictive, while inheriting behavior is not.
Agree with @StefanKarpinski +1 for not requiring subtypes to inherit fields.
Given Julia's target market, fields being large arrays will be likely to naturally occur, so forcing subtypes to inherit them would be costly if the subtype doesn't need them.
Given Julia's target market, fields being large arrays will be likely to naturally occur, so forcing subtypes to inherit them would be costly if the subtype doesn't need them.
@elextr, I'm a little confused by this point. Since I don't believe it will be possible to instantiate abstract types, it's not obvious to me how the size of arrays (a property of objects, not of types) could pass between abstract types and concrete types. Did I miss part of the conversation?
@johnmyleswhite as I understand the discussion it is about allowing abstract types to have fields that can be inherited by subtypes and at some point a concrete type that can be instantiated with those fields.
The point is that requiring the inheritance of such fields could be expensive if the field is an array, but the methods on the concrete type did not use that information, as @StefanKarpinski said.
@StefanKarpinski But you can always choose not to put fields in the abstract type. It's only there if the design calls for it.
@elextr Likewise, if not all concrete types require that array, it shouldn't be in the abstract type. You should only have the fields that are common to all concrete types.
If you use the grouping technique as suggested before (i.e. make another type that collects all common fields) you have the exact same problem. But if you don't group in a common type then you potentially have to copy and paste code in order to make sure no field name changes.
@abeschneider that presumes that a hierarchy is designed at one time by one group who "control" the entire hierarchy. But one of the key things about Julia is the existence of a package system to publish such hierarchies for use by others.
To the initial designer of the abstract type provided by a package it may make perfect sense to include fields in line with their initial use-case. But Julia users can create their own subtypes adding implementations that the original abstract type designer never thought of.
Forcing fields to be inherited may impose unreasonable costs on the new implementation. But since some other users of the abstract type may use the inherited fields, they cannot simply be removed without breakage.
It should be up to the subtype implementation to decide if it needs to inherit the fields from the parent type. It is of course then the subtype implementer's responsibility to overload any parent method that uses fields they do not inherit.
@elextr That's true with many other languages besides Julia (e.g. Python and Ruby).
A library could in theory do something like:
abstract BaseType
abstract BaseTypeWithFields <: BaseType
# fields
end
type SubType1 <: BaseTypeWithFields
# ...
end
type SubType2 <: BaseType
# ...
end
This gives the most flexibility as it:
@abeschneider This solution is really not that different from:
abstract BaseType
type BaseTypeFields
# fields
end
type SubType1 <: BaseType
base::BaseTypeFields
# ...
end
type SubType2 <: BaseType
# ...
end
If it comes to compiler checks it is in my opinion much more important to validate public interfaces (e.g. methods) instead of internal fields. In general I think if you want to convince people you might have to show a larger code project where the lack of abtract types with fields makes the code ugly. That the issue actually does not come up very often on the Julia mailing list (and issue tracker) is more a sign that the lack of this feature is not such a big issue in practice. And the number of Julia packages is not that small anymore.
I have a type named FinancialTimeSeries
that wants to reuse all the available methods in the TimeArray
type found in the TimeSeries package. Here is the current TimeArray
design
immutable TimeArray{T,N} <: AbstractTimeSeries
timestamp::Vector{Dates}
values::Array{T,N}
colnames::Vector{UTF8String}
end
FinancialTimeSeries
needs some more information (stock ticker, tick size, currency, etc) so to achieve this there are three alternatives.
The first one simply copies the TimeArray
code and extends it. The only method that is reusable is the length
method.
immutable FinancialTimeSeries{T,N} <: AbstractTimeSeries
timestamp::Vector{Dates}
values::Array{T,N}
colnames::Vector{UTF8string}
metadata::Stock
end
The metadata branch in TimeSeries tries to solve this upstream by adding a new filed called metadata
, but now the TimeArray
type is looking cluttered.
immutable TimeArray{T,N,M} <: AbstractTimeSeries
timestamp::Vector{Dates}
values::Array{T,N}
colnames::Vector{UTF8string}
metadata::M
end
A sort of imperfect way of establishing a FinancialTimeSeries
type with a new metadata TimeArray
is to define typealias FinancialTimeSeries{T<:Float64,N,M<:AbstractInstrument} TimeArray{T,N,M}
@tknopp your suggestion above could define FinancialTimeSeries
without cluttering the original TimeArray
by doing something like this:
immutable FinancialTimeSeries{T,N} <: AbstractTimeSeries
series::TimeArray{T,N}
metadata::Stock
end
The problem with is nesting is that you would need to redefine getindex
and other methods. This can be done with one-liners though
getindex{T,N}(ft::FinancialTimeSeries{T,N}, n::Int) = getindex{T,N}(ta::TimeArray{T,N}, n::Int)
But, the real trouble with nesting like this is a new type called Blotter
that keeps track of financial transactions.
type Blotter
start::Array{Date}
finish::Array{Date}
series::FinancialTimeSeries
end
So now we we have a nested type that includes a nested type. Does this cause a performance hit? I would imagine so but I haven't tried it to find out.
@tknopp My point was that allowing fields in abstract types doesn't impose any constraints on the user.
I don't think it's difficult to imagine a realistic example given in #7442 with 5-10 more fields, at which point the design you gave above becomes difficult to maintain if you have a decent number of Player
subtypes. One project I'm working on has just that. @milktrader also posted another example while I was typing my response.
It may not come up a lot on the mailing lists, but I don't think that doesn't mean it isn't useful (or there aren't people who would use it). Also, multiple inheritance, which you have argued for (and I agree with), only shows a handful of hits when I search on the Julia user list.
The problem with using methods to define the public interface is that the compiler isn't doing any checks for you. Sure, you know the method exists, but you don't know if the subtypes actually have fields you are accessing. Thus, you will only find the error at runtime, and are therefore just moving the problem around, but not actually solving it.
@milktrader What about this:
abstract AbstractTimeSeries
immutable AbstractTimeSeriesFields{T,N}
timestamp::Vector{Dates}
values::Array{T,N}
colnames::Vector{UTF8string}
end
# Now there are all the methods that operate on an AbstractTimeSeries
# they assume that every AbstractTimeSeries has a field "base" of type AbstractTimeSeriesFields
# One example follow:
length(t::AbstractTimeSeries) = length(t.base.timestamp) # don't no if this is the correct length... but you get the idea
immutable TimeArray{T,N} <: AbstractTimeSeries
base::{T,N}
# Might need a "nicer" constructor that hides AbstractTimeSeriesFields
end
# Despite the constructor TimeArray needs 0 new methods
immutable FinancialTimeSeries{T,N} <: AbstractTimeSeries
base::{T,N}
stock::Stock
# Might need a "nicer" constructor that hides AbstractTimeSeriesFields
end
# Again all the AbstractTimeSeries methods are automatically available.
I don't think that there will be any performance hit in the indirection but if you are concerned about speed you have to benchmark it.
@abeschneider It is true that it is not such a good measure how often something comes up on the mailing list. Abstract multiple inheritance is something where no simple workaround is available and which is quite standard in other "interface" languages such as Java and C#. Further, there are some packages where this was/is really needed but the discussions where not on the mailing list but on github issues. After all we can duck-type as a valid workaround which leaves all type checking out however.
But anyway, I am not a core dev and just wanted to convince you that the lack of abstract fields with types can be easily emulated with simple means and without cluttering the code. But since you are not convinced maybe the best way to get this in is to prepare a PR that implements this. I would vote for merging it in the case that overloading field access will stay unpossible.
This seems to have been fairly well-exhausted with the consensus leaning towards not having it. Objections to closing?
It was my understanding that it was still under consideration, just not necessarily in the near future. That might also just be wishful thinking.
I wrote a quick macro to do abstract types with fields on the user-side:
https://github.com/abeschneider/AbstractFields.jl
It's very simple, and could probably use more work, but I think it's argument for not introducing 'Abstract Types with Fields' into the language directly (at least for now). If people want the feature, they can use it, and if it's considered useful enough, it's possible to reconsidered for inclusion into the language.
On a side note, it would be nice if extra syntax was allowed for macros. Currently to use the macro you have to do:
@_type foo parent begin
# ...
end
It would look more seamless (and perhaps help testing new features) if I could write:
@_type foo <: parent
end
@abstractfields type foo <: parent
# ...
end
will work fine. Choose whatever macro name you want, of course. This is the approach we take in StrPack.
Wouldn't a combination of #1974, #6975 and using a concrete type similar to what @tkopp showed that could optionally be added nullify this issue?
There would be the concrete type with the 'standard' fields used in the type and the interface then requires the getfield
and/or setfield!
methods to be defined.
The user could then just use the default methods defined on the abstract type by using the 'standard' type or use their own fields and define a type specific method.
Something like this (excuse if I have misunderstood something in #1974):
abstract foo
type fooFields
bar::Int
baz::Float32
end
interface fooInterface
getfield :: (x::fooInterface, ::Field{:bar}) --> Int
getfield :: (x::fooInterface, ::Field{:baz}) --> Float32
setfield! :: (x::fooInterface, ::Field{:baz}, input::Float32) --> Int
end
Then to be able to use the fooInterface
the required overloaded field accesses must be implemented. For implementation either the concrete type would either use fooFields
or their own fields but concreteType.bar
and concreteType.baz
will always be present if the fooInterface
is used.
@Mike43110 I think you might just be moving the problem to a difference place. If you have to define the field accessors per type, that seems like it's only creating more work. Am I missing something? Compare that to:
@_abstract Foo begin
bar::Int
baz::Float32
end
@_type FooChild Foo begin
# bar and baz already exist here
end
function do_something{T <: Foo}(foo::T)
# this will always be safe to do
foo.bar = 5
end
All the boiler-plate code is taken care of by the compiler and the semantics are clear (well, would be clearer if they weren't implemented purely in macros).
If I haven't made a grave misunderstanding somewhere, the field accessors would only have to be implemented on the abstract type and then again only if the user wants to use their own fields instead of the ones defined in the container.
I have missed one thing that your proposal would not have to worry about. That the container field should always have the same name.
I just feel that interfaces 'fit' as they can both define what methods are available and, with field access overloading, also what fields are available. The internal implementation is not a worry unless you want to have different internal fields.
But, it does boil down to that this can be solved in other ways apart from directly attaching types to abstract types.
That being said I have shifted the problem somewhere else, the interface. The amount of work should be the same except for when the internal fields of the class are not using the container type.
Though the same thing could be done currently just by using your macro and then no access overloads would be required.
I just wanted to address the concern of inheriting structure vs. behaviour by making some of the structure part of the behaviour while not directly requiring that the structure actually be the same.
@Mike43110 Okay, I see, I didn't quite get how the interface was working, but it makes sense now.
But, yes, you still have to copy the fields for the child classes, which is my main complaint. For simple cases, it's not so bad, but even for not-so-big cases (e.g. PEGParser or GrammaticalEvolution) it's added a decent amount of time due in terms of debugging (I have a shallow class hierarchy that shares many of the same types) due to typos.
As for the argument for separation of behavior from structure, maybe a third type could work:
abstract Foo
trait FooTrait
bar::Int64
baz::Float64
end
type FooImpl(FooTrait) <: Foo
bat::String
end
where a trait would allow composition of fields, but would have no bearing on types. Types can then pull from multiple types to construct their fields and from abstracts to define their behaviors.
The container type performs the same function as the trait.
I should probably have included an example of a small implementation. This will be using the above definitions. It won't be 100% correct as interfaces have not been finalised but it should serve as a basic example.
type implementedfoo <: foo
#The inheritance here could be a single class if abstract types are interfaces
#Which would be @mauro3 's suggested syntax
container::fooFields
#The container could be rewritten if so desired but is unecessary
end
implement foo <: fooInterface
getfield(x::fooInterface, ::Field{:bar}) = x..container..bar
getfield(x::fooInterface, ::Field{:baz}) = x..container..baz
setfield!(x::fooInterface, ::Field{:baz}, input::Float32) = setfield!(x, :x..container..baz, input)
#The above line may be incorrect but it should get the point across
#This will work for any foo that has a field container which holds fields bar and baz
end
This should be more clearer. Or at least I hope it is! The container type would then remove your worry about copying fields, it is just a single field that needs to be implemented. It can also be implemented in a different fashion if the user so desires. A different implementation would then require that the interface be re-implemented to have the field accesses correct.
The downside is I can see some "extending" the container class by implementing it as a field with extra fields. This may occur multiple times which would be quite ugly as it could turn into something ridiculous like x.container4.container3.container2.container.baz
.
On the traits idea, aren't you then creating something similar to Java's abstract classes and interfaces? The major difference being that a trait can't provide method definitions like Java's abstract classes can via full definitions or abstract methods.
How would the constructors for traits work? Would the user have to go back and check what fields the trait they are using has?
@Mike43110 Yes, I agree. Sorry, I didn't mean to say it wouldn't perform the same function. I think there are a couple ways to implement field-sharing. The issue I have with the containers method you sketched out is that it makes field-sharing a second-class citizen. I think it should be something that requires almost no extra work and shouldn't fundamentally change the syntax (i.e. you shouldn't have to know that the final class was derived from other classes).
The traits
idea was actually mostly stolen from Scala. Traits would provide a method to define structure that is completely separate from behavior (as is the stated goal of many people on this thread). Abstract classes define the behavior, and types themselves mix the two together.
The point of mixing in trait
s to your class is their fields, so you would have to know about their fields anyways. In Scala traits don't allow constructors in order to simplify the construction rules, and it might make sense to do something similar.
Here's a quick example:
abstract Player
trait Entity
id::Int64
x::Float64
y::Float64
end
# The type User copies the fields of Entity and inherits the behavior of Player
type User(Entity) <: Player
name::String
score::Int64
team::Array{User}
# Some order rule is needed for which fields to fill first. In this example, the trait is filled first.
User(name::String, team::Array{User}) = new(get_next_id(), select_user_location()..., name, 0, team)
end
# don't add any new fields in this case, but change the behavior
type Asteroid(Entity) <: Player
Asteroid() = new(get_next_id(), select_asteroid_location()...)
end
# ...
function collides(a::User, b::Asteroid)
# ...
end
I wrote a quick example of trait
s using macros (warning: almost completely untested, though using most of the code from the AbstractFields):
https://github.com/abeschneider/TypeTraits.jl
The runtests.jl
has the following:
abstract Player
@trait type Entity
id::Int64
x::Float64
y::Float64
end
@mixin type User(Entity) <: Player
name::String
end
# currently inner constructors are not supported
function User(name::String)
User(0, 0, 0, name)
end
user = User("Foobar")
println(user) # User(0,0.0,0.0,"Foobar")
I forgot to thank @pao for the suggestion of using macros on types.
No problem--what you've done looks pretty similar to what StrPack does now, in fact.
I guess with #20418 we could now consider abstract struct ... end
I really hope this feature to be implemented soon. I am porting a C++ code, where a superclass with lots of member fields is inherited by hundreds of subclasses. I think the standard way of implementing this kind of situation in Julia is to copy all the fields of the superclass to subtypes as
abstract type AbstractMyType end
struct MyType1 <: AbstractMyType
x1 # unique to MyType1
a # all fields from this line and below are common to AbstractMyType
b
c
...
end
struct MyType2 <: AbstractMyType
x2 # unique to MyType2
a # all fields from this line and below are common to AbstractMyType
b
c
...
end
Copying many member fields to hundreds of subtype definitions is tedious. (I think https://github.com/JuliaLang/julia/issues/19383 was an attempt to overcome this problem.) Also, if I want to add additional member fields to the supertype, I need to add them to all the subtypes. The proposed feature will eliminate these problems.
In the mean time, my workaround was to define a type MyCore
containing all the shared member fields and make it a common member field of subtypes:
struct MyCore
a
b
c
...
end
abstract type AbstractMyType end
struct MyType1 <: AbstractMyType
x1 # unique to MyType1
core::MyCore # common to AbstractMyType
end
struct MyType2 <: AbstractMyType
x2 # unique to MyType2
core::MyCore # common to AbstractMyType
end
This approach solves the aforementioned problems, but it implicitly assumes that all the subtypes have core
as a member. For example, consider the following function that handles all the subtypes of AbstractMyType
:
myfun(m::AbstractMyType) = m.core.a + m.core.b + m.core.c
Here, I had to require that any subtype of AbstractMyType
has core
as a member field, but there is no mechanism to force this requirement to subtypes.
Recently I came up with a nice method to solve this issue using a parametric type. In this method, I replace AbstractMyType
with a parametric type with a parameter that describes specs distinguishing individual subtypes, and define MyType1
and MyType2
as type aliases of AbstractMyType
with type parameters corresponding to particular specs:
abstract type AbstractSpecs end
struct AbstractMyType{S<:AbstractSpecs}
specs::S
a # all fields from this line and below are common to AbstractMyType
b
c
...
end
struct Specs1 <: AbstractSpecs
x1 # unique to MyType1
end
struct Specs2 <: AbstractSpecs
x2 # unique to MyType2
end
const MyType1 = AbstractMyType{Specs1}
const MyType2 = AbstractMyType{Specs2}
Now, you can define functions for AbstractMyType
(which is not really abstract by the way) without referring to implicit member fields like core
in the previous example:
myfun(m::AbstractMyType) = m.a + m.b + m.c
Also, unlike the method proposed in https://github.com/JuliaLang/julia/issues/19383, the <:
relationship holds between the subtypes and supertype:
julia> MyType1 <: AbstractMyType
true
julia> MyType2 <: AbstractMyType
true
I think this is a practical method to use in my current situation of porting the C++ code, but I haven't seen it widely used in the Julia community. (Maybe I am ignorant.) Hope this helps people struggling with similar situations until the proposed feature is implemented.
Complex parameterized types in Julia are far more common than abstract types with "hundreds" (or even dozens) of subtypes, so I would say in that sense that this is a standard technique in Julia. That being said, if your C++ code consists of hundreds of subclasses, my first reaction is that you should perhaps completely re-think the code organization when porting it to Julia.
@stevengj, I also use parametric types extensively in all of my projects, but their particular usage, combined with type aliasing, to mimic the behavior of the inheritance in OOP languages as shown in the above example didn't occur to me until recently. When I needed inheritance, I always used an abstract type and defined its common member fields in all the subtypes (and manually defining the common member fields in all the subtypes will become unnecessary once abstract types with fields proposed in this thread are implemented).
The C++ code I am porting right now has hundreds of subclasses of one superclass, corresponding to hundreds of devices of an abstract device. Because the code supports hundreds of different devices, I think such a large number of subclasses are unavoidable. But I agree that the number of subclasses is overwhelming, and it will be nice to reduce it. I will look into the possibility of reducing the number of subclasses; maybe they can be grouped into a few subcategories. Thanks for the advice!
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 https://github.com/JuliaLang/julia/pull/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.
I find this explanation reasonable. However, I think that this feature would be very useful in some applications.
This would look something like
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.