Workaround for traditional inheritance features in object-oriented languages


#15

Julia doesn’t have classes so it seems pretty self evident for me that it isn’t suited for “class-based OOP”… So yeah, trying to design your type hierarchies exactly as you would in OOP is likely not going to be optimal.

Comments like that are just inflammatory. Please consider the case that you haven’t actually understood the problem and the implications of “fixing” it.

Same here. It is often better to take the position of a student than a teacher. Do you really feel your expertise is high enough that you can state what you are stating as confidently as you do.


#16

as a general rule, the best practice is to work with the platform, and not against the platform. OOP thinking should be left at the door, because Julia is not an OOP language. you need to approach the problem with the tools you have at hand, not trying to reimplement the tools you used to have.


#17

Real-world problems aren’t well-suited for inheritance-based OOP either, which is why you’re generally told/taught to not use inheritance in OOP, except in your first undergrad class where you learn about OOP and program design. Here’s three links discussing why considered using these features is considered bad practice (even in OOP languages).

So that begs the question, if every style guide is against inheritance and for composition, why do people push so hard to have inheritance implemented when composition comes very natural to Julia through multiple dispatch?

Let me elaborate on that last part a bit. Say you want

type B <: HoldsAnA
  a::A
end

to act like an A in many places. Then you can f(b::B) = f(b.a) and dispatch handles the rest (and implicit returns makes that function that easy). You can even make an entire “class” of types “inherit” this ability via f(b::HoldsAnA) = f(b.a).

You can do multiple inheritance of behavior via traits. For more information on that, check out SimpleTraits.jl. Essentially, you can make some type have a trait, and just dispatch functions on that trait. You can use that to make functions give a value:

@traitfn islinear(x::::IsLinear) = true
@traitfn islinear(x::::(!IsLinear)) = false

so instead of looking for “an inherited field” x.islinear, this is “an inherited function call” islinear(x)… but guess what? Unlike a field of a mutable object, this is known at compile time and thus results in faster code!

Now lets say you got to here and are like, well I still want inheritance. Well, you can take the 10 minutes to make it yourself. A poor man’s way is just via @def:

macro def(name, definition)
    return quote
        macro $(esc(name))()
            esc($(Expr(:quote, definition)))
        end
    end
end

You can use this macro to do compile-time copy/paste, which is essentially what inheritance of fields is. So you can do:

@def the_fields begin
  x::Float64
  y::Float64
end

type A
  @the_fields
   z::Float64
end

type B
  @the_fields
end

and there you go. What if you want to go all of the way? Here’s a good introduction to meta-programming project: implement this design:

@inheritable X{T} begin
  x::{T}
  y::Float64
end

@inherits type A
  X
  z::Float64
end

@inherits type B
  X
end

The tricky part is making it produce A{T} (but make it general enough so you can have X{T} with z::T and get A{T,T2}. Hint: use gensym), but all of the information is there. So if you still really Really REALLY need OOP-style inheritance, there you go: it’s a fun weekend project.

This question gets asked enough that this response might need to be a blog post so I can just paste it around.


#18

This question gets asked enough that this response might need to be a blog post so I can just paste it around.

Please do, @ChrisRackauckas!

@juliohm Once you can get past the idea that current “OO” techniques are somehow better than the flexible type system (getting much better shortly, when #18457 is merged), combined with multiple dispatch and emerging things such as traits, I think you’ll find yourself amazed at the power available in Julia, and you’ll end up feeling greatly limited in most any other language out there.


#19

Why? Look, I did not mean my comment to be inflamantory and I don’t understand why you feel that it is. I thought that I was objective and polite. Why is it wrong to say that X problem is needlessly hard in Y language? There isn’t a language in the world that is ideally suited for every problem. I could name any language in the world and you could easily list problems that it makes needlessly difficult.

What are you talking about? I did not suggest a fix. I said “think about whether there is a reasonable way the language could be improved.” — There is an implicit recognition that maybe there isn’t any reasonable way to improve the language. I don’t claim to know whether there is a reasonable solution or not.

I agree. And I thought I did. At least, that was my intention.

What am I stating? That I think we should admit that Julia cannot do X and that people smarter than me might want to think about whether there is a reasonable way to improve the language? I don’t see how I could have made the statement milder unless I am not allowed to say that Julia isn’t suited for class-based OOP, but you yourself agreed that this is the case in the first sentence of your response. I don’t understand why my post is inflamatory and yours isn’t when you said the same thing that I said.


#20

Thanks. I’ll have a look when I get a chance. But to be clear, I did not ask for OOP features. If I wanted to write inheritance-based OOP I’d just pick up Python or Ruby rather than fight with Julia. I use Julia for numerical stuff like data analysis. I use Julia for problems that Julia is meant to solve and solves well.


OOP in Julia, inherit from parametric composite type
#21

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.


#22

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?


#23

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.


#24

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”: https://en.wikipedia.org/wiki/Mixin

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.


#25

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


#26

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.


#27

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: http://mooseframework.org/docs/doxygen/moose/classTransientInterface.html

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.


#28

Does this solve your problem?


#29

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.


#30

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!


#31

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


#32

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


#33

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:


#34

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.