Multiple inheritance carry-over members

Inheritance vs. mulitple dispatch in julia is maybe one of the hard things to grasp. I am still struggling with it and therefore the question. Does julia have a concept of carrying over data members to derived classes? I have read about multiple dispatch being what replaces the need for multiple inheritance and I can see how this may be true for methods, but what about data? To demonstrate, here is an example using @tholy ’ s class AxisArray:

julia> V = AxisArray(rand(3); row='a':'c')
1-dimensional AxisArray{Float64,1,...} with axes:
    :row, 'a':1:'c'
And data, a 3-element Vector{Float64}:
 0.3122957215062174
 0.6611468595604877
 0.6639367902035573
julia> V = AxisArray(rand(3); row='a':'c')
1-dimensional AxisArray{Float64,1,...} with axes:
    :row, 'a':1:'c'
julia> V2 = V .+ 0
And data, a 3-element Vector{Float64}:
 0.3122957215062174
 0.6611468595604877
 0.6639367902035573

As it is seen, the addition of 0 already removes the nice axis properties from the array. Even more so, also views of the array burry its nice properties:

julia> V3 = reshape(V,(1,3))
1×3 reshape(::AxisArray{Float64, 1, Vector{Float64}, Tuple{Axis{:row, StepRange{Char, Int64}}}}, 1, 3) with eltype Float64:
 0.312296  0.661147  0.663937
julia> V3.axes
ERROR: type ReshapedArray has no field axes

One may now argue that it is in the responsibility of the package author to write multiple-dispatch methods such that for example such views are also supported. But this is a task which would not be necessary in ordinary inheritance schemes where the data members are essentially just copied and new data members can appended.
The problem: Am I missing an important julia feature or way of usage in my toolbox, or is this a current limitation of julia or an unavoidable limitation?
In my mind the usefulness of AxisArrays (and all programming which adds data to existing structures) is extremely limited if the user always have to keep track of the axis data. In this case a separate axis class is more useful (which may of course exist already), but it just seems wrong, since a set of axes should belong to the array and should be propagated along with what is done to the array.
I guess the broadcasting mechanism can somehow be carried over to AxisArrays but what about the views? How about introducing a julia keyword persist, which tells a data member to conceptually automatically reside (or be accessible from) structure of the encapsulating structure?
This way it would be fairly easy to deal with the views, as the axis would “blubble up” though a series of such encapsulations and still be visible. But then this needs some change to the type structure, as the resulting view type needs to still ideally be recognized a subtype of AxisArrays and not just AbstractArray, I guess.

No. The design of Julia go against this on purpose. The common example is the Square class. If I create a Rectangle class, then makes sense to have Square as a subtype of rectangle, because this replicates the mathematical relationship between them, however, Rectangle has two attributes (length and width, for example) and for Square it would be better to inherit nothing and instead just use a single attribute to store the value (saving half the memory).

From your examples, however, I see nothing related to inheritance.

The first examples is related to how broadcast was implemented for AxisArray. I do not know intimately the broadcast machinery, but I believe materialising Vector is the default (i.e., it works even if you do nothing about it), but you can adapt its behaviour, for example, trying to broadcast over Dict or NamedTuple errors (it would work automatically if it was not reserved by the core team), (1, 2, 3) .+ 1 returns a Tuple. So here you should contact Tim Holy to know if this behavior of broadcast is intended or not.

A view of some array is not a subtype of the wrapped array. ReshapedArray is a subtype of AbstractArray as much as AxisArray is a subtype of AbstractArray, ReshapedArray is not under AxisArray, or any other Array type it can wrap. This is not common behavior on most languages that I know, because the relationship of a wrapper type is “contain a” not “is a”. I mean, Ruby and its infinite dynamism probably could delegate any method the wrapper does not know to the wrapped object, but it does not stop being a “contains a” relationship and not a “is a” relationship because of that.

While we are talking about broadcast, this is probably some behavior Tim just did not implement. Not a limitation of the language.

I do not believe generic views can deal with what you are asking here, you can maybe create a specialized type of view, because the generic one is made to work with any AbstractArray. Luckily, you can create a view type that recognizes other methods that may be applied to your type, and overload some Base function to use your specific type of view when your specific type of array is passed.

Yes, this kind of dynamic type creation may be possible, but probably indicates you are trying to use Julia as something it is not.

2 Likes

Regardless of the internal layout of a Square object, it would be nice if I could count on Square instances having a length and width interface just like Rectangle does. Currently square.length errors at runtime and length(square) (which is equivalent but more verbose) does too.

You have two solutions there, the first one is that length and width should be methods implemented by any subtypes of AbstractRectangle (or the adequate type). The second is to overload getproperty to make square.length and square.width work even if the internal layout is just a single side attribute.

Any of the two ways will need explicit implementation, there is no way to make this work in an entirely generic way, because, as I pointed out, if the internal layout is different, as in the Square type case, then you will need to inform the compiler that both length and width must be obtained from side.

I am not sure what you meant by “equivalent but more verbose”, length(a) and a.length are two completely distinct things, you can make them mean the same, but this is by no way default behaviour.

1 Like

The problems with AxisArray seem to be orthogonal to inheritance? I mean at least both NamedDims.jl and DimensionalData.jl keep dimension names and behavior after view and broadcasting and similar operations - they have what you might call “dominant” wrapper types that try to rewrap during methods like these. But these are just implementation details - wrappers like AxisArray can behave pretty much any way you want by writing custom implementations of these base methods.

But… keeping names after reshape doesn’t seem like a good idea, the axes really aren’t the same things after that.

Lastly, I once wrote a package to add field inheritance via mixins (Mixers.jl) and I never use it now. Just use composition instead.

4 Likes

Julia does not have inheritance.

There are no “derived classes” in Julia. The pattern you may be looking for in general is composition.

That said, the particular problem you highlight with AxisArray may be solved by hooking into the broadcast interface and propagating the extra information. Maybe open an issue for that package, or make a PR.

1 Like

Thanks for the explanation and for pointing me towards DimensionalData.jl. Indeed, it supports the broadcasting scheme, which works well. Yet, my main point was a mechanism that works (be “dominant”) with encapsulating views. I tried DimensionalData.jl and it failed to do its job in combination with for example a PaddedView. I suspect that this is a fundamental problem. I do understand that PaddedView can add some code to accommodate DimensionalData and possibly also the other way round, but the combinatorial effects of everybody having to accommodate everybody else’s additional data are just too large to make sense. Can you maybe explain a bit more about “composition”? Can this mechanism achieve this? Or can broadcast somehow penetrate also the “view” via parent()? If all of this is not possible, there might be at least some generic implementation such as the AbstractArray supporting a dictionary for “additional stuff” to be carried along (and then another encapsulation can retrieve this information), or a best practise such that the looked for information can be found by traversing the parent() tree? But this will probably create a mess in assuring types.

and it failed to do its job

I think you misunderstand the scope of what you want here. It’s job doesn’t really extend into the constructors of ecosystem packages that I don’t know about, I can’t see how this would “just work” in any language. How do we influence the PaddedView constructor? and what should happen if you try to wrap it with a NamedDims.jl wrapper? which should “win”?

But it is possible to solve this with very little boilerplate code, and as usual in Julia, it would need some (a lot of) coordination and a shared interface (as you suggest with AbstractArray, but probably actually with traits and shared methods in an interface package like ArrayInterface.jl).

The simple case would be that PaddedView has a method that dispatches on DimArray and rewraps. But to generalise to AxisArrays,jl and NamdDims.jl and any other wrapper type, we could have a method in ArrayInterface.jl - rewrap(f, A). PaddedView and other array packages could call rewrap in their constructor. The default rewrap would just call f and return it. But DimensionalData.jl could add a method to unwrap the internal array, run the constructor f, and wrap it with a DimArray again.

Someone would have to write this, and packages would have to implement it. Probably a lot wouldn’t, but as ArrayInterface.jl is more widely accepted, things like this may start to work?

Broadcast can handle this a little better - it’s possible to make DD always “win” as the outer wrapper. I’m not sure how well it goes currently with other packages. Probably NamedDims.jl wrappers would beat it as @oxinabox understands broadcast better than I do.

Composition is what we are doing by wrapping objects that already have behaviors, instead of inheriting behaviours from a supertype, as we are with AbstractArray. But I meant that comment in relation you question about field inheritance, which is actually unrelated to this problem. Regular inheritance doesn’t apply in this context because PaddedView does not inherit from or know about AbstractDimArray, it inherits from AbstractArray - so composition is all we can do.

This is a problem of agreeing on ecosystem wide shared interfaces, and working out what they need to do and agreeing to implement them. Maybe some of them will make it into Base one day. That’s what ArrayInterface.jl is for, but these things are a long way off.

Personally, I just use DimensionalData.rebuild for this kind of thing:

rebuild(some_dimarray, PaddedView(parent(some_dimarray), args...))

Which of course will break the index if PaddedView changes the array size, another complication to this issue. Doing that generically would need rewrap(f, A, I...)

See: interface for rewrapping Array wrappers · Issue #136 · JuliaArrays/ArrayInterface.jl · GitHub

1 Like

AxisArrays.jl is a bad example.
It was never fully updated with the broadcasting changes in Julia 0.6 or 1.0.
I recommend: AxisKeys.jl, AxisIndicies.jl or DimensionalData.jl as modern replacements.
We’re planning a BoF at JuliaCon this year to workout a full plan to officially deprecate AxisArrays.jl

I should write a blog post.
There is basically one correct way to implement 70% of the broadcasting stuff if you are a wrapper array.
I suggest copy pasting the code from NamedDims.jl and tweaking that.

(Which itself is copied+cleaned from DistributedArrays.jl)

The fact that one does have to do this does seem like our polymorphism isn’t powerful enough.
Though perhaps we are just missing a bit of our abstraction.
E.g. a macro could generate that stuff and they wouldn’t require a fundamental change to the language to increase power…
And more power is not nesc good.
We have a lot of expressive power as is.
Some features that increase it would open up to known problems.
E.g. there is a writeup somewhere that explains why inheriting fields leads to problems. I think it might be the Fragile Base Class problem.
Another example is being able to define what happens on method error. Lua let’s you do that. But that would result in having to add a bunch of backedges (i.e compilation cache invalidation hooks) to methods that would force recompilation far more often (in particular those hooks were once that were removed in 1.6 because not needed and are part of why Julia 1.6 has much faster package loading).

3 Likes

That was my strategy (youre even thanked in the source), but I think you made some changes since then.

Doing broadcast properly does seem a dark art unfortunately, and since your code works I haven’t justified the time to learn it.

1 Like

I think this particular problem has been addressed by Adapt, that seems like a good general solution that various array libraries could (should?) support.

That would work for arrays that need to propagate to the bottom, like CuArray.

The CuArray constructor could call adapt(CuArray, A) instead of CuArray(A), which would work already with DimArray already as it supports Adapt.jl:

julia> adapt(CuArray, DimArray(view(rand(10, 10), 1:5, 1:5), (X, Y)))
DimArray (named ) with dimensions:
 X (NoIndex)
 Y (NoIndex)
and data: 5×5 view(::CuArray{Float64, 2}, 1:5, 1:5) with eltype Float64
 0.774018  0.428549   0.811505   0.676999   0.474822
 0.5862    0.944905   0.0430036  0.750278   0.819485
 0.698396  0.982062   0.646442   0.0306205  0.870356
 0.259098  0.0646769  0.526254   0.850805   0.489501
 0.309116  0.679976   0.346363   0.400562   0.243139

But it wont cover a lot of other cases.

1 Like

In OOP even ordinary inheritance (with data members) is essentially based on extending structures by additional data and additional methods. In Julia this only concerns methods and data needs to be wrapped by subtyping from AbstractArray and making one data member a final array.

In Python you can make your own array class as a child of the numpy array class (np.ndarray). I have done this and let my class have a different standard display method (like Image in Julia) and also dimension functionality (like DimensionalData). When the base class (np.ndarray) does “its job” it simply does not care and does not need to know that the structure is really longer. It also relies on some standard mechanisms to not loose those extra members. Numpy already has a mechanism in place that can “rewrap” its result back into you derived type. All you need to do is to overwrite this wrapup function and it pretty much works out of the box. The main point I am making here is that np.ndarray is agnostic about what other classes may be derived from it and it can still “do its job” (i.e. calculations on arrays) and even return the derived type, if wanted. Some sort of finalize function is called provides you with the necessary information to for example carry over the dimension information (e.g. via “similar” construction). Wrt “who should win” in OOP an object can be both, an instance of its own class as well as being an instance of all parent classes (including their data structure).
I do understand and appreciate that Julia on purpose does not support this, but then it would be nice to have an easy-to-use generally accepted rewrapping mechanism in place. Maybe “Adapt” does this, but I did not yet have a look at it.

I think from a user perspective there are two possible ways:

  1. The ecosystem has a generally agreed way of rewrapping
  2. The user has an easy way to define which package should be working with which other package(s).

I do think that this is really a practical problem in Julia as currently this problem of combining features appears to be common. E.g. try to calculate with the Image class. Even if you deprecate AxisArrays.jl, many other subtypes of AbstractArray quickly lose their nice properties when it comes to working with views which limits their practical use. It may well be easier to keep track of dimension information separated from the arrays than to deal with the various propagation problems.

But you are extending the type yourself! The key point of misunderstanding here is that PaddedViews.jl does not extend AbstractDimArray or DimensionalData.jl at all.

If it did, that would also just work. Thats what a GeoArray does.

AbstractDimArray is entirely agnostic about what classes are derived from it. It was intentionally written as a generalised abstraction, where AxisArray isn’t (one of the reasons I wrote a new package). Of course, they need to define the fields again, or define diferent fields with new methods that point to them (it’s a method based interface, fields are never accessed directly). But that’s a minor problem compared to what you are talking about.

You are asking for completely separate objects to be rewrapped, which is an orthogonal problem to inheritance - it’s about having a widely implemented shared interface.

So, just so we’re clear: PaddedView is not an instance of AbstractDimArray. There is no inheritance involved here.

2 Likes