Workaround for traditional inheritance features in object-oriented languages

I am using the Julia type system seriously for the first time and have just learned that it doesn’t support inheritance from non-abstract types nor multiple inheritance. This statement applies to Julia v0.5.

Could you please explain how one can workaround these limitations, specifically:

  1. How you develop code with inheritance from abstract types only? Does it cover all possible use cases?
  2. Is there any workaround for the lack of multiple inheritance?
3 Likes

(1) Inheriting from concrete types is often considered to be a design mistake in the languages that support it. It’s not clear to me that there’s any use case in which it’s required. It’s hard for me to imagine a case where inheritance from concrete types allows one to express something that couldn’t be achieved using composition, although I can imagine that inheritance removes some tedious repetition in some kinds of code.

(2) I think most Julia developers feel that a traits system would be superior to multiple inheritance for Julia. See Rust’s traits for an example of how traits are used in a modern language; Haskell’s type classes and C++'s concepts are also good topics to investigate. There are already several libraries that provide macros that provide basic trait-like functionality for Julia: I believe both Mauro and Andy Ferris have libraries for this.

5 Likes

To “work around” the absence of multiple inheritance approaches misframing the need. Julia’s features and facilities are multi-capable by design; and with some familiarity you will learn they intra-strenghen in application . The language does not underpin a computer software designer;s toolbox; it is different from systems that are as tools to the tool-makers. Julia facilitates coding the expressively obvious because Julia eases expressing collegial communication (processes and ideas). This “working with Julia’s way of working” is a better approach than would be “working around” an omission of initial familiarity.

5 Likes

When I first moved from Python/C++/Matlab to Julia, I did find myself missing concrete inheritance. But as I learned more about how good Julia code is structured, that feeling faded.

One concrete suggestion I can offer is to focus on composition instead of inheritance. Where in Python you might do:

class A:
    foo
    bar

class B(A):
    b

class C(A):
    c

in Julia you might choose to do:

type A
    foo
    bar
end

type B
    a::A
    b
end

type C
    a::A
    c
end

which has the added benefit of cleanly encapsulating all of the A-like behavior inside a single type, rather than mixing it into the behavior of B and C.

9 Likes

@johnmyleswhite thank you for your answer. The tedious repetition you mention in (1) is the need for redefining every single method with the composite type? This is a big one.

Can you give an example of how traits can replace multiple inheritance?

Can you please comment on how this approach scales? It doesn’t seem practical to me, consider this simple example:

type Person
  name::String
  age::Int
end
age(p::Person) = p.age

type Employee
  person::Person
  id::Int
end
age(emp::Employee) = age(emp.person)

emp = Employee(Person("John",30),1)
age(emp)

Every time I instantiate a composite type, I need to explicitly call the constructor of the base type. Of course we can add sugar constructors to avoid this typing, but it is definitely tedious. Most annoying is the fact that we need to redefine (or wrap) every single method that applies to the base type so that it works with the derived type (e.g. age method).

Can you please explain how this approach is better than just having traditional inheritance?

In your example a hasa relation does not make to much sense from my point of view
Here would be my proposal:

abstract AbstractPerson
age(p::AbstractPerson) = p.age
name(p::AbstractPerson) = p.name

type Person <: AbstractPerson
  name::String
  age::Int
end

type Employee <: AbstractPerson
  name::String
  age::Int
  id::Int
end
id(p::Employee) = p.id

emp = Employee("John",30,1)
age(emp)

The point is actually to think about the interface that you want for your types. The interface should be a set of functions that the type is going to implement. You see some repetition in the above code but in practice this is not a big issue if you prevent deep type hierarchies.

3 Likes

@tknopp this is a very disconnected interface, the type system doesn’t know Person and Employee are related. Also, in this snippet of code you do not only provide the interface, you implement it in the base, which is not practical with slightly more complicated methods. The implementation may (and will likely) change depending on the type.

The fact that an Employee is also a Person is quite important to keep things organized in the system.

Sorry I missed to inherit from the abstract type. Is corrected in the above example

Got it, thanks. We still have the issue of implementing the interface in the base with this approach.

This is not really an issue depending how pure your thinking is. The methods

say that any subtype needs to implement these two and that p.age and p.name are the default fields that will be used. You are free to override this by a definition

name(p::Employee) = "Superman"
1 Like

Its also important to note that these are all just the low level methods. If you implement high level algorithms that involve the methods age and name you will always implement against the abstract interface and in turn only have to provide a single implementation. So with larger code the issue actually gets less relevant.

Regarding the concrete inheritance, this thread is very useful:
https://groups.google.com/d/topic/julia-users/qj-ivya6l8A/discussion

The basic idea is that if your goal is code reuse, then use composition, not inheritance. Inheritance is for common behavior, and behavior is implemented in methods in Julia, and those can be defined on an abstract type and overridden for any other type in the hierarchy. The tradeoffs are discussed at length in the links @ChrisRackauckas posted in that thread.

1 Like

I agree with @juliohm here. As much as I like Julia, it really doesn’t seem well-suited for class-based OOP. In the “employee vs person” example, the problem is very naturally stated in the language of concrete inheritance. Julia seems to be taking a simple problem and making it needlessly hard. I think we should just admit that Julia doesn’t do a good job here and maybe think about whether there is a reasonable way the language could be improved.

4 Likes

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.

9 Likes

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.

5 Likes

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.

15 Likes

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.

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.

6 Likes

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.