Workaround for traditional inheritance features in object-oriented languages

Hello again. Thanks again for the links. So now I’ve mostly read through them. The message that I get is that composition and inheritance both have value and one cannot replace the other. The last article you listed makes that point explicitly:

The Wikipedia page makes roughly the same point. Wikipedia gives both benefits and drawbacks to composition, and the main drawback is having to re-implement methods. They gives an “employee” problem, similar to the one from @juliohm, as an example of something that works better with inheritance.

Anyway, the articles were interesting and they convinced me that both techniques have a valuable role to play ideally neither one would be neglected.

1 Like

keep in mind that Julia does offer OOP features, including runtime polymorphism, inheritance (although more limited and/or tedious than other langs) and dynamic dispatch. just not as pronounced as in some other languages.

but what is not there?

  1. information hiding is on the weaker side.
  2. you can’t inherit data members.

that’s pretty much it. did i miss anything?

Inheritance definitely has value, though it’s often abused where composition would work as well or better. Julia provides inheritance with the limitation that only the leaf nodes of the inheritance hierarchy can hold state. While this is in some situations limiting, it has the benefit of simplifying a lot of things about how the language works, making code easier to reason about, and IMO is a worthwhile trade-off.

I think the main downsides to this approach come up when you want to define methods on the abstract types that need to manipulate the state held in the concrete types, particularly if there are multiple pieces of state. I think the two main issues that come up are:

  1. How do you access the state
  2. What happens when you want to change the way the state is stored (for example adding some state field)

I think for the first problem you can either specify that subtypes use specific names for state fields and that they have all required state, or you define accessor functions that subtypes need to implement. Either of these are somewhat tedious and error-prone if you have lots of state fields.

The 2nd issue could be a real pain - if you need to add a required field, do you need to add it to all subtypes? gross. I think a reasonable solution here is to have a MyTypeState type that packages up all the state needed by the abstract implementations, and all subtypes are required to have a field with that type. That way you minimize the requirements that subtypes have to satisfy. You could use either of the approaches for #1 above and it doesn’t seem too bad. For the employee example it would be something like:

abstract Person

type PersonState
    name::String
    age::Int
end

name(p::Person) = p.state.name
age(p::Person) = p.state.age
function havebirthday!(p::Person)
    p.state.age += 1
end

type PlainPerson <: Person
    state::PersonState
end

type Employee <: Person
    state::PersonState
    id::Int
end

function downsize!(e::Employee)
    e.id = -1
end

Now you can add both methods and state at the abstract level without touching the subtypes. This would get cumbersome (but doable) if you wanted to have a multi-level type hierarchy with different sets of state for different subtrees, but I’d argue that you’re probably on architecturally shaky ground there anyways. Other languages might make that sort of thing easier to do with concrete inheritance, but I’d rather read code that made the complexity explicit rather than having all that polymorphic field access stuff happening automatically in the language. It also makes you second guess whether you reeeally need to do that, which is probably healthy.

Regarding multiple inheritance, I think that a lot of people agree that the language could use something a little more first-class, but it’s a remarkably tricky problem. From what I can tell it’ll probably end up looking like traits of some kind, but I haven’t used any of the existing traits implementations and don’t know much about it.

I’ll throw my hat in the ring here for wanting some amount of “member data” inheritance.

As already rehashed here: composition works well in many cases, but isn’t the best tool for all jobs.

Once you get outside of really simple datastructures that only hold a few member variables (which are the only ones we like to write down in forum posts!) composition can get unwieldy quickly. In particular: multiple levels of composition creates very leaky abstractions with the only alternative being a lot of shim/pass through code.

Inheritance can work to “flatten” these hierarchies… hiding the details of how the hierarchy is implemented and presenting a uniform interface for developers to work from. This helps with both code reuse (obviously) and with protecting code against future changes in the library.

Consider a hierarchy that requires a “train wreck” like this to access the piece of data called “b”:

something.somethingelse.otherstuff.junk.b

Each of those levels came from many nested levels of composition. If you were using inheritance instead you could just do:

something.b

Where b here came from inheriting the chain something → somethingelse → otherstuff → junk

Not only does this code look simpler… but it also hides the details where b comes from, from this line of code. Suppose we want to rename junk to newjunk. If you’re not using inheritance then you have to go fix all the instances of the first line throughout your whole code base. If you are using inheritance then you just change the name of the class you inherit from and nothing else.

Yes: in Julia you could implement a b() function that did this for you for the specific type of something… but you would also need to do it for sometingelse and otherstuff… and if you are dealing with MANY data members this gets completely out of control. In addition, each time you go to extend the hierarchy you have to reimplement the shims…

So… to get a bit more concrete:

My area is finite element simulation… where every computation is embedded down on the inside of many nested loops and requires a LOT of data. You might have something like 40-50 pieces of individual data flowing into one line of code down on the inside of a quadruply nested loop. That data comes from many places: geometry, shape functions, material properties, variable values, time integration, parallel distribution, etc.

You could hold member variables to all of the “data stores” and create lots of “train wrecks” to access down through them to dig out the data you want… or you could flatten all of that using inheritance to provide a nice, flexible, change-resistant API.

Let’s look at a concrete example from my project called “MOOSE” ( http://mooseframework.org ). Lets look at a “Postprocessor”… think of it as a scalar reduction across the domain: do an operation on each piece of the domain and produce a scalar value. In this particular case we want to compute the average value of something:

The base-class that you inherit from to implement a custom averaging operation is here:
http://mooseframework.org/docs/doxygen/moose/classElementAverageValue.html

A “user” of MOOSE (who is a code developer creating a custom finite element simulation tool) inherits from that class and implements computeQpValue() to compute the thing down on the very inside of the loop. All of the inheritance you see there in the class diagram is working to provide tons of data and functionality to that person that they can use to form the computation they want to take the average of.

Let’s say they want to take the average of the the current time (which doesn’t quite make sense, it’s just a simple example). Because of inheritance their code will look like:

TimeAverager::computeQpValue()
{
  return _t;
}

With composition it would look like:

TimeAverager::computeQpValue()
{
  return element_average_value.element_integral_variable_pp.element_integral_pp.element_uo.transient_interface._t
}

Not only is that second line unwieldy… but it has MANY “reasons to change”. If any of those objects in-between change then this line of code has to change. Whereas, in the inheritance case, the inheritance hierarchy can change significantly without affecting “user” code.

In fact! It has! Postprocessor existed many years before UserObject… we came in and placed more fundamental base classes underneath Postprocessor a few years ago… all without affecting a single line of user code!

In addition, composition increases the cognitive load on a consumer of an object. Instead of just seeing all of the data available to them through inheritance (like on that Doxygen page) they have to move through all of the objects to see what’s available. In the case MOOSE we don’t even want our users to know that many of those levels/object exist! They are there for code-reuse purposes and architectural purposes… but aren’t useful for our users know about!

In this way Inheritance can provide a uniform “data interface” to other data… which is somewhat different from the way people usually think of “interfaces”. A related idea to using inheritance in this way is called “Mixins”: Mixin - Wikipedia

Anyway - I’m getting off on a tangent now. My point is that inheritance can do a lot more than just provide an “is-a” relationship. It can help you build deep hierarchies that present data as nice, rolled up extension points for consumers of your library.

Currently I don’t see any way to do something similar in Julia without a bunch of trickery/hackery/copying/shims.

3 Likes

In Julia, wouldn’t it be more natural for each object to implement computeQpValue(x), so that each object would only need to know about one level below it? i.e. you would have computeQpValue(x::TimeAverager) = computeQpValue(x.element_average_value), which would in turn call computeQpValue for element_average_value.element_integral_variable_pp, and so on? That way, if you change things, e.g. to add a level of composition, only the object where you add the new composition needs to change its method.

@jiahao put it to me once that in a multiple-dispatch language, the emphasis is more on the relationships between verbs (functions) than between nouns (objects).

5 Likes

but in julia, you would not do that. when you implement junk, you would provide functional access to the b part, as it is appropriate. accessing junk.b anywhere other than methods of junk is already a bad practice. and then when you implement otherstuff, you would provide some access to junk. yet again, calling otherstuff.junk is ugly anywhere else. for oop people, you can consider data members private, and only access methods public.

an argument can be raised immediately: isn’t that a lot of bookkeeping? with OOP, if i inherit, i immediately get all the public access methods at once. however, this is a double edged sword, because yes you get all of them, but you can not not get all of them. in contrast, julia offers full control, and also offers an effective way to implement methods en masse, for example:

for fn in (:start, :done, :next, :eltype, :size)
  @eval Base.$fn(o::MyType, etc...) = Base.$fn(o.subobj, etc...)
end

i understand that it isn’t the same, in particular it will not implement methods that are added later to the subobject. just saying that the situation is not as bleak as it might seem at a glance.

It’s hard to provide an example that’s complicated enough to show the issue without being incomprehensible. It seems I failed :slight_smile:

Let me try to clarify:

computeQpValue() is pure virtual. There’s nothing to call down the chain. The levels of inheritance are (for the most part) adding layers of data that are available to the leaf classes.

In Julia computeQpValue() would definitely be implemented as computeQpValue(x::TimeAverager) and multiple dispatch would admirably fulfill the roll of the virtual function (In fact: I personally believe that multiple dispatch does this job much better than virtual functions for many reasons I won’t go into here).

The problem is how to get the data that’s needed in computeQpValue() for the computation. There are a few options:

1: You “dig”. You do things like x.element_average_value.element_integral.element_uo.time_interface.time

2: You provide single layer “shim” functions like:

time(x::TimeInterface) = x.time
time(x::ElementUserObject) = time(x.time_interface)
time(x::ElementIntegral) = time(x.element_uo)
time(x::ElementAverageValue) = time(x.element_integral)
computeQpValue(x::TimeAverager) = time(x.element_average_value)

3: You use inheritance

(#1) is a bummer for all the reasons I listed earlier.

(#2) is what a lot of Julia codes end up doing now… but it is completely unworkable for large numbers of member variables and deep hierarchies. Check out the massive number of member variables and methods inherited by AuxKernel: http://mooseframework.org/docs/doxygen/moose/classAuxKernel.html to provide shims for each of those would be a major bummer. In addition every time you change one of the fundamental classes to provide a new piece of data you would have to create thousands of shim functions.

Just look at how many objects end up inheriting _t from TransientInterface: www.mooseframework.org: TransientInterface Class Reference

BTW: This is not academic. I’ve actually implemented a massively parallel, multiphysics, finite-element framework using Julia: https://github.com/friedmud/MOOSE.jl and I’m running up against this problem. For what I have done at this point there is nothing too egregious… but if I take it further I’m going to end up in a big mess without some sort of inheritance.

3 Likes

Does this solve your problem?

This is really getting to be a FAQ. Here’s another approach by Mike Innes with his @forward macro that helps with the bookkeeping of your option #2.

2 Likes

I think that the existence of so many workarounds / FAQs is a clear indication that “first class” support for inheriting from concrete types in the language itself would be welcomed by many users. Essentially, that’s me seconding what @danielc said earlier, and like him, I’m not trying to be inflammatory here: just trying to provide some feedback to help with the direction of Julia.

The war over composition vs. inheritance and the value of multiple inheritance (vs. its implementation complexity) have been debated for MANY years across many forums for many languages. From what I’ve seen there is no one “right” answer: the combination of the problem being solved and the preferences of the people solving it ultimately leads to different implementations… and that’s ok.

For me: it’s just a bummer because I really love so much about Julia but this one thing keeps smacking me in the face. For my own personal projects I’ll try to use some of the workarounds proposed by @pint and especially @Tem_Pl (ConcreteAbstractions does look good)… but it’s difficult to suggest basing large, long-term projects around workarounds. But maybe as I use Julia more I’ll see different ways of solving these problems and it will be less of an issue.

Thank you everyone for the discussion so far!

4 Likes

I hear. I think @StefanKarpinski had discussed putting delegation facilities into the language.

2 Likes

“Extensions” might be better than “workarounds”. ConcreteAbstractions is 61 lines of code. The @forward macro is eight lines of code. In both cases, it’s easy to follow what they do.

Implementing concrete type inheritance in base would encourage poor designs. Plus, basic inheritance is never enough. Users will want to inherit from two or more types, or they’ll want just some parts of one type, or they’ll want some terms renamed. The Modelica language is a big OO modeling program based on inheritance. Feature upon feature has been added to make inheritance work “better” to the point where the language spec is huge, and it makes the language hard to use.

4 Likes

Lack of inheritance is not a “feature” in my book. It can be “worked around” using “extensions” but that doesn’t make it a “good” thing.

Julia has many great concepts and many awesome implementations using those concepts. It’s a fundamentally useful language but this is definitely a gap many programmers will feel.

After-all: Julia already contains most of the bones you need for object-oriented design. In fact, I believe that Julia does many object-oriented things better than “traditional” object-oriented languages. For instance: “static polymorphism” is natural in Julia, there is no need for the CRTP or SFINAE and “templated virtual functions” are possible. This is really the last thing “missing” and I think that’s one of the reasons this will always be fairly sought-after capability by Julia’s users.

I know it is for me. I want all of that multiple-dispatch goodness… with just a sprinkling of type inheritance :slight_smile:

Then what’s wrong with the macros? Any Base implementation would be essentially the same thing, where you just take the @ out of the syntax.

1 Like

I’d like to make something clear. I have never used ConcreteAbstractions. I’ve written a lot of Julia code, and I’ve never needed it. I built it in less than an hour to stop these sorts of threads from forming. Can we please talk about something else. :pray:

7 Likes

May not be anything “wrong” with them - I’m going to need some more time with them.

However, if they were in Base and they were “blessed” as being the “preferred” way then they would be used consistently. The way it is right now, if I’m using 3 different packages they may all use different type extension mechanisms. Not only would that cause cognitive dissonance as you switch between pieces of your code that work with each package and you need to re-orient yourself to how you need to deal with inheritance in that part of the code… but it may also mean that you won’t be able to combine types from multiple packages together (because they’re using different extension systems).

It also makes it harder to move between working on different packages developed by different teams.

@tbreloff would it be ok if we switch to talking about the need for public, protected, private and friends in Julia?? :wink:

Yeah, I agree in that I’d like to see it in Base, but I just don’t see the urgency. It’ll be nice when it comes. And for anyone who finds it to be urgent, they should be the one to start the PR :slight_smile:

1 Like

I hope Julia doesn’t evolve to encourage deep hierarchies of types.

Having worked some with Java, where deep hierarchies are common, my experience is they make large chunks of the ecosystem unproductive for use in small ad hoc projects, where Julia should excel.

It is frustrating to spend time learning a hierarchy of classes to instantiate an object just to call some relatively straightforward method.

7 Likes

Thank you all for the discussion, it is being great. We definitely need a documentation/wiki page discussing a slightly more complicated example. This example should illustrate the advantages/disadvantages of using composition and shallow hierarchies in Julia over the traditional deep hierarchies and inheritance. Can someone with experience write an example in very accessible language?

I support the idea of shallow hierarchies too, it makes code more “reusable” in a sense. I just don’t know to what extent they can represent the complexity of moderately large software.

3 Likes

They can build Julia. Well, actually no inheritance was used there, of course.

2 Likes