About inheritance and abstract types

There’s something about Julia style that I’m just not getting. Let’s say I want a Curve type for a curve in the complex plane. It’s meant to be abstract, since I will have different subtypes of Curve (Line, Circle, etc). But all objects of Curve type should have a pos field that gives the position z as a function of a parameter. This is all that’s needed for a lot of Curve functionality.

In a purer OOP language I’d have Curve as an abstract class that has a pos property and methods that work on all Curves. All those methods know that the object has a pos property, even though the class is abstract.

In Julia, if I have an abstract Curve type and methods that operate on all Curves, those methods will need to use the pos property. Every concrete subtype of Curve then has to be responsible for declaring a pos property, rather than having it be guaranteed as part of Curve. That’s a recipe for mayhem.

Essentially I’m looking for the concept of an interface. What is the Julia equivalent? Or does Julia have another way to look at this?

2 Likes

With the ‘interface’ style, you might be looking for something like

abstract type Curve end

get_pos(c::Curve) = error("No get_pos method defined for curve type $(typeof(c))")

type Line <: Curve
pos::...
end

get_pos(line::Line) = line.pos

do_something(c::Curve) = get_pos(c) + ...

and you’ll get an appropriate run-time error if a given Curve subtype doesn’t conform to the implicit get_pos interface. There is discussion around adding language-level support for a formal interface concept in a future release of Julia.

7 Likes
using Graphics: @mustimplement

abstract type Curve end

@mustimplement get_pos(c::Curve)

...

the same just a little bit more terse using the must implement macro

2 Likes

Its also not just shorter but IMHO it reads really nice and makes the code self documenting. I use this a lot to program against defined interfaces and it works pretty well.

what can I find this @mustimplement macro?

Ah thanks - somehow I misread that this was in Base. I use a similar macro and also find it very useful and clear

It was in base but went to the Graphics package. Actually it does not really belong there but the discussion on interfaces in Base have kind of stopped.

If you want to also “inherit” the behaviour, you can define a fallback method for the abstract type (rather than an error), and override only when required for concrete sub-types.

abstract type Curve end
get_pos(c::Curve) = c.pos

type Line <: Curve
    pos::Float64
end

type Arc <: Curve
    pos::Float64
end
get_pos(arc::Arc) = arc.pos + 1

type Circle <: Curve
    centre::Float64
end

get_pos(Line(2.5))      # use default fallback
get_pos(Arc(2.5))       # use Arc-specific method
get_pos(Circle(2.5))    # ERROR: type Circle has no field pos

One advantage to the Julia approach is dealing with degeneracy. For example, the point (1.0,1.0) is a 0-dimensional curve that could be represented as:

type One <: Curve end
get_pos(one::One) = (1.0, 1.0)

In this example, the savings are trivial, but in general it allows for degenerate subtypes of an abstract supertype to be as light as they can be. If a family of types were to contain a matrix, then it’s reasonable for a degenerate subtype to instead have the identity, I, that doesn’t need to be stored in every concrete instance of the degenerate subtype.

1 Like

On that note, how is it possible to get something common to all types extending the abstract type. For example, if I have a field called type::String. Do I need to implement get_type in every subtype? Is it possible to have a common get_type defined at the base (abstract or not abstract) level.

get_type(curve::BaseType) -> "curve", "straight-line", "circle" depending on how it was constructed?

If all subtypes have the field, then

get_type(curve::BaseType) = curve.type

works. And if some subtype then goes and puts that info elsewhere, then you can special-case it:

get_type(curve::SomeSubType) = curve.some_other_field
3 Likes

if all subtypes have the field

I think the above is key. Thanks for the answer, that’s exactly how I’m addressing that, but code duplication grows pretty quickly without inheritance.

It might be possible to make a macro that makes Julia act as if it had inheritance. The rough idea would be to store a dict keyed on abstract types who’s values were vectors of fields to be inherited.

There are quite a few of those out there, many are listed in this thread:

1 Like

If the duplication comes from repeating the fields, consider using composition.

4 Likes

I think that this guide is useful for users coming to Julia with OO mindset:

Also you may want to use this package for your specific question:

1 Like

There is also https://github.com/mauro3/OO.jl :wink:
(please don’t use it!)

3 Likes

if the duplication comes from repeating the fields, consider using composition.

Thanks for the input. My 2 cents is that Julia can benefit from OO if we want to use it in large projects. My understanding is that composition, while nice, still doesn’t save from code duplication in all derived types.

More than the mindset, it is the practical aspect of maintaining large code bases. Many projects are switching from Fortran to C++ in my field due to the multiparadigm and modern features availability. While I like Julia and what it offers, it is still hard to sell the alternatives to OO as it is very similar to Fortran. type dispatch is great, but I think it’s orthogonal/complementary to inheritance. I’d still use Julia for small projects, though.