JuliaLang / julia

The Julia Programming Language
https://julialang.org/
MIT License
45.39k 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.

pbazant commented 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.

pbazant commented 10 years ago

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".

tknopp commented 10 years ago

@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.

abeschneider commented 10 years ago

@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.

abeschneider commented 10 years ago

@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.

mauro3 commented 10 years ago

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.

tknopp commented 10 years ago

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.

abeschneider commented 10 years ago

I can see where it is an issue for overloading the . operator, but is it also an issue for abstract types with fields?

elextr commented 10 years ago

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.

tknopp commented 10 years ago

@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.

tknopp commented 10 years ago

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.

stevengj commented 10 years ago

@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.

abeschneider commented 10 years ago

@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.

tknopp commented 10 years ago

@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.

abeschneider commented 10 years ago

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.

tknopp commented 10 years ago

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.

abeschneider commented 10 years ago

@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).

StefanKarpinski commented 10 years ago

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.

abeschneider commented 10 years ago

@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.

StefanKarpinski commented 10 years ago

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.

elextr commented 10 years ago

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.

johnmyleswhite commented 10 years ago

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?

elextr commented 10 years ago

@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.

abeschneider commented 10 years ago

@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.

elextr commented 10 years ago

@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.

abeschneider commented 10 years ago

@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:

  1. Doesn't impose fields in the way that you are worried about
  2. Provides a method to write more generalizable code (i.e. you know the fields exist in the method so you don't have to: (a) check the existence of the field, or (b) deal with errors if the field doesn't exist).
tknopp commented 10 years ago

@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.

milktrader commented 10 years ago

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.

abeschneider commented 10 years ago

@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.

tknopp commented 10 years ago

@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.

tknopp commented 10 years ago

@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.

quinnj commented 10 years ago

This seems to have been fairly well-exhausted with the consensus leaning towards not having it. Objections to closing?

abeschneider commented 10 years ago

It was my understanding that it was still under consideration, just not necessarily in the near future. That might also just be wishful thinking.

abeschneider commented 10 years ago

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
pao commented 10 years ago
@abstractfields type foo <: parent
# ...
end

will work fine. Choose whatever macro name you want, of course. This is the approach we take in StrPack.

mikewl commented 9 years ago

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.

abeschneider commented 9 years ago

@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).

mikewl commented 9 years ago

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.

abeschneider commented 9 years ago

@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.

mikewl commented 9 years ago

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?

abeschneider commented 9 years ago

@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 traits 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
abeschneider commented 9 years ago

I wrote a quick example of traits 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")
abeschneider commented 9 years ago

I forgot to thank @pao for the suggestion of using macros on types.

pao commented 9 years ago

No problem--what you've done looks pretty similar to what StrPack does now, in fact.

stevengj commented 7 years ago

I guess with #20418 we could now consider abstract struct ... end

wsshin commented 3 years ago

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.

stevengj commented 3 years ago

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.

wsshin commented 3 years ago

@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!

StefanKarpinski commented 3 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 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.

RaulDurand commented 3 years ago

I find this explanation reasonable. However, I think that this feature would be very useful in some applications.