Introduction

using ModuleMixins

@compose module A
  struct S
    a
  end
end

@compose module B
  @mixin A

  struct S
    b
  end
end

fieldnames(B.S)
(:a, :b)

A struct within a composed module can be mutable and/or @kwdef, abstract base types are also forwarded. All using and const statements are forwarded to the derived module, so that field types still compile.

using ModuleMixins

@compose module A
  const V = Vector{Int}
  struct S
    a::V
  end
end

@compose module B
  @mixin A
end

typeof(B.S([42]).a)
Vector{Int64} (alias for Array{Int64, 1})

Diamond pattern

The following pattern of multiple inheritence should work:

using ModuleMixins: @compose

@compose module A
    struct S a::Int end
end

@compose module B
    @mixin A
    struct S b::Int end
end

@compose module C
    @mixin A
    struct S c::Int end
end

@compose module D
    @mixin B, C
    struct S d::Int end
end

fieldnames(D.S)
(:a, :b, :c, :d)

The type D.S now has fields a, b, c and d.

Motivation from OOP

Julia is not an object oriented programming (OOP) language. In general, when one speaks of object orientation a mix of a few related concepts is meant:

  • Compartimenting program state.
  • Message passing between entities.
  • Abstraction over interfaces.
  • Inheritence or composition.

Where in other languages these concepts are mostly covered by classes, in Julia most of the patterns that are associated with OOP are implemented using multiple dispatch.

The single concept that is not covered by the Julia language or the standard library is inheritance or object composition. Suppose we have two types along with some methods (in the real world these would be much more extensive structs):

struct A
    x::Int
end

amsg(a::A) = "Hello $(a.x)"

struct B
    y::Int
end

bmsg(b::B) = "Goodbye $(b.y)"

Now, for some reason you want to compose those types so that amsg and bmsg can be called on our new type.

struct C
    x::Int
    y::Int
end

amsg(c::C) = amsg(A(c.x))
bmsg(c::C) = bmsg(B(c.y))

There are some downsides to this: we needed to copy data from C into the more primitive types A and B. We could get around this by removing the types from the original method implementations. Too strict static typing can be a bad thing in Julia!

An alternative approach would be to define C differently:

struct C
    a::A
    b::B
end

We would need to abstract over member access using getter and setter methods. When objects grow a bit larger, this type of compositon comes with a lot of boilerplate code. In Julia this is synonymous to: we need macros.

There we have it: if we want any form of composition or inheritance in our types, we need macros to support us.