Skip to content

Composition Subclassing

Lszaboo edited this page Nov 8, 2025 · 3 revisions

Object-oriented like type system of Juliagebra

On this page you can read about how Juliagebra handles OOP through composition.

We need to have a type system where we can easily define object-oriented like classes that have:

  • Inheritance
  • Abstraction
  • Dynamic dispatch

Type system setup

Or first step to achieve the points mentioned above starts by defining the abstract class setup of our program using the built-in abstract types of Julia

abstract type VehicleDNA end
abstract type CarDNA <: VehicleDNA end
abstract type SuperCarDNA <: CarDNA end

Here we define three abstract classes:

  • Vehicle
  • Car
  • Supercar

The "body" of theese classes will be created later. Here we just define them, so that in later files we can start using theese classes as function inputs, without theese classes being defined yet (so this is how we defend ourselves from circular imports).

Note here that their Julian abstract type name ends with "DNA" everywhere.

Abstract types

Now let's start defining Vehicle first:

# ! Vehicle is an "abstract class"
mutable struct Vehicle
    _passengerCount::Int
end

_Vehicle_(self::VehicleDNA)::Vehicle = error("Missing func!")

getPassengerCount(self::VehicleDNA) = _Vehicle_(self)._passengerCount

We can see that Vehicle is defined as a struct.

Note here that when writing we use VehicleDNA as an input type. This is because Vehicle is an abstract class, and force that we can't use it as is, only children of Vehicle can actually exist.

The Vehicle function will play a crutial role, as theese functions will be defined in the children, and they will help Julia know where the parent's data is in the children and so theese function should be named like ParentName with two "_"-s.

Observe that Vehicle function is used in the getPassengerCount function, where if someone is a children of Vehicle, it should have a Vehicle compositionally, so we ask for it, and return _passengerCount, because Vehicle has that field.

Abstract children of Abstract types

Let's see how Car is defined:

# ! Car is an "abstract class"
mutable struct Car
    _vehicle::Vehicle
    _fuel::Float64

    Car(passengerCount,fuel) = new(Vehicle(passengerCount),fuel)
end

_Car_(self::CarDNA)::Car = error("Missing func!")
_Vehicle_(self::CarDNA)::Vehicle = return _Car_(self)._vehicle

getFuel(self::CarDNA) = return _Car_(self)._fuel

As we can see Car extends Vehicle. To tell Julia that this is the case, we define the Vehicle function as seen above and make Car have Vehicle as a field. For setting up the class to have children, we define Car as well.

Concrete classes

Concrete classes are classes which can't have children, but can be constructed and polymorphism works on them.

# ! Helicotper is a "Concrete class"
mutable struct Helicopter <: VehicleDNA
    _vehicle::Vehicle
    _altitude::Float64

    Helicopter(passengerCount,altitude) = new(Vehicle(passengerCount),altitude)
end

_Vehicle_(self::Helicopter) = return self._vehicle
getAltitude(self::Helicopter) = return self._altitude

Note that Vehicle has to be defined and Helicopter has to have a Vehicle typed field for full subclassing.

Concrete classes with children

Now let's finish up the remaining DNA, SuperCarDNA:

# ! SuperCar is a "Concrete class" and it can have children as well
mutable struct SuperCar <: SuperCarDNA
    _car::Car
    _color::String

    SuperCar(passengerCount,fuel,color) = new(Car(passengerCount,fuel),color)
end

_SuperCar_(self::SuperCarDNA)::SuperCar = error("Missing func!")
_SuperCar_(self::SuperCar)::SuperCar = return self

_Car_(self::SuperCarDNA) = _SuperCar_(self)._car

getColor(self::SuperCarDNA) = return _SuperCar_(self)._color

The only big difference is that SuperCar is defined twice, and SuperCar is Julian subtype of SuperCarDNA.

Note that children for SuperCar should be defined like this:

# ! Ferrari is a "Concrete class"
mutable struct Ferrari <: SuperCarDNA
    _superCar::SuperCar

    function Ferrari()
        passengerCount = 2
        fuel = 50.0
        color = "Red"
        new(SuperCar(passengerCount,fuel,color))
    end
end

_SuperCar_(self::Ferrari) = return self._superCar

Notes

Example usage of the above defined type system:

ferrari = Ferrari()

println("$(getPassengerCount(ferrari))")
println("$(getFuel(ferrari))")
println("$(getColor(ferrari))")

lamborghini = SuperCar(2,96.0,"Orange")

println("$(getPassengerCount(lamborghini))")
println("$(getFuel(lamborghini))")
println("$(getColor(lamborghini))")

helicopter1 = Helicopter(5,0.0)

println("$(getPassengerCount(helicopter1))")
println("$(getAltitude(helicopter1))")

helicopter2 = Helicopter(15,670.0)

println("$(getPassengerCount(helicopter2))")
println("$(getAltitude(helicopter2))")

One very powerful advantage of this type system, is that whenever I write functions for parent classes, inside thoose functions I can acces the full reference to the children as well, not just the compositional part of the class.

The compositional functions ("ParentName") are not that performance heavy, because Julia can inline theese, but one could write @inline just to make sure.

For Generic classes (for example "GenericClass{T} - GenericClassDNA{T}") this pattern should be followed (Julia even has Generic abstract types).

This implemention of Object-orientedness can be made shorter with macros, but for debuggability it remains typed.

Wiki navigation sidebar

Legend:

  • ✅: Up-to-date
  • 🏗️: Needs some work
  • ❌: Unusable
  1. 🏗️ Home
  2. 🏗️ Developer Guide
    1. Composition Subclassing
    2. Core Graph
    3. 🏗️ Dependents
    4. 🏗️ Observer and Observed
    5. 🏗️ Plans
    6. 🏗️ Rendered Dependents
    7. Macro Constructors

Clone this wiki locally