The key feature of the package is the management of an abstract type hierarchy that defines the subclass and superclass relationships desired of the the concrete types representing the classes. The concrete types defined for each class include all the fields defined in any declared superclass, plus the fields defined within the class declaration. The abstract type hierarchy allows methods to be defined for a class that can also be called on its subclasses, whose fields are a superset of those of its superclass.

The Classes.jl package includes the macro, @class, and several exported functions, described below.

The @class macro

The @class macro does all the real work for this package. For each class (say, MyClass) created via @class, the following code is emitted:

The 'mutable' keyword

Class mutability is specified by including mutable before the class name, e.g., @class mutable MyClass ... Note that mutability is not inherited; it must be stated in each subclass if mutability is required.

Functions emitted by the @class macro

The @class macro emits



Initializers are functions that set values on an existing class instance. These come in several forms:

Reflection methods

Defining methods on a class hierarchy

To define a function that operates on a class and its subclasses, specify the associated abstract type rather than the class name in the method signature. Since the subclass contains a superset of the fields in the superclass, this works out fine. Subclasses can override a superclass method by redefining the method on the more specific class.


@class Foo begin

@class Bar <: Foo begin

compute(obj::AbstractFoo) = obj.i * obj.i

Since Bar <: AbstractBar <: AbstractFoo, the method also applies to instances of Bar.

julia> foo = Foo(5)

julia> compute(foo)

julia> bar = Bar(4, 3.3)
Bar(4, 3.3)

julia> compute(bar)

We can redefine compute for class Bar to override its inherited superclass definition. Note that we can use the type AbstractBar, which allows this method to be "inherited" by subclasses of Bar, or we can use Bar directly, in which case the method applies only to this concrete type.

julia> compute(obj::AbstractBar) = obj.i * obj.f
compute (generic function with 2 methods)

julia> compute(bar)

Subclasses of Bar now inherit this new definition, rather than the one inherited from Foo, since the prior class is more specialized (further down in the shadow abstract type hierarchy).


using Classes

@class Foo <: Class begin

   Foo() = Foo(0)

   # Although Foo is immutable, subclasses might not be,
   # so it's still useful to define this method.
   function Foo(self::AbstractFoo)
        setfield!(self, :foo, 0)

@class mutable Bar <: Foo begin

    # Mutable classes can use this pattern
    function Bar(self::Union{Nothing, AbstractBar}=nothing)
        self = (self === nothing ? new() : self)
        Bar(self, 0)

Produces the following methods:

# Custom constructors defined inside the @class above

# Methods emitted by @class macro for Foo

# all-fields constructor

# local-field initializer
Foo(self::T, foo::Int64) where T<:AbstractFoo

# Custom constructor defined inside the @class above

# Custom initializer defined inside the @class above
Bar(self::Union{Nothing, AbstractBar})

# Methods emitted by @class macro for Bar

# all-fields constructor
Bar(foo::Int64, bar::Int64)

# local-fields initializer
Bar(self::T, bar::Int64) where T<:AbstractBar

# all fields initializer
Bar(self::T, foo::Int64, bar::Int64) where T<:AbstractBar

#  Superclass-copy initializer
Bar(bar::Int64, s::Foo)

Example from Mimi

The following diagram shows the relationship between the concrete structs and abstract types create by the @class macro. Solid lines indicate subtype relationships; dotted lines indicate subclass relationships, which exist outside the julia type system.

Mimi component structure

Each class as a corresponding "shadow" abstract supertype (of the same name surrounded by underscores) which is a parent to all abstract supertypes of its subclasses.