Skip to content


Parametric types without the type parameter are NOT DataTypes; they are UnionAll.

Example: struct Foo{T} end; isa(Foo, DataType) == false;

I find it easiest to write model specific code NOT using parametric types. Instead, I define type aliases for the types used in custom types (e.g., Double=Float64). Then I hardwire the use of Double everywhere. This removes two problems:

  1. Possible type instability as the compiler tries to figure out the types of the custom type fields.
  2. It becomes possible to call constructors with, say, integers of all kinds without raising method errors.

Broadcasting (1.6)

For an object that behaves like a scalar, such as

struct Foo
    x :: Int

it is enough to define Base.broadcastable(f :: Foo) = Ref(f).

Constructors (1.5)

Constructing objects with many fields:

  • Define an inner constructor that leaves the object (partially) uninitialized. It is legal to have new(x) even if the object contains additional fields.
  • LazyInitializedFields.jl ensures that accessing uninitialized fields gives errors.

Parameters.jl is useful for objects with default values.

  • Constructor must then provide all arguments that do not have defaults.
  • Note that @with_kw automatically defines show(). Use @with_kw_noshow to avoid this.
  • Base.@kwdef now does much of the same.

Dicts (1.7)

Removing a key from a Dict: delete!(d, k).

Inheritance (1.5)

There is no inheritance in Julia. Abstract types have no fields and concrete types have no subtypes.

There are various discussions about how to implement types that share common fields. For simple cases, it is probably best to just repeat the fields in all types.

One good piece of advice: ensure that methods are generally defined on the abstract type, so that all concrete types have the same interface (kind of the point of having an abstract type).

Macro for common fields

A macro that lets users define a set of common fields for a set of structs:

macro def(name, definition)
    return quote
        macro $(esc(name))()
            esc($(Expr(:quote, definition)))

@def commonfields begin
    X #Feature vectors
    y #Labels (-1,1)
    nSamples::Int64 # Number of data points
    nFeatures::Int64 # Number of features

struct Foo

But this does not work with default values. Another option using @eval.

A more robust implementation is Mixers.jl. @mix is intended to create parametric types. But @mix C1{} [...] works as well and creates a plain vanilla type. However, the @pour macro is then simpler:

julia> @pour c1 begin
   x :: Int
   y :: Float64
@c1 (macro with 1 method)

julia> struct Foo
          z :: String

julia> Foo(1, 2.0, "abc")
Foo(1, 2.0, "abc")

A more recent implementation is CompositeStructs.jl. Not necessarily better than Mixers.jl, but works well for me.

Common fields with default values

mutable struct Foo

    Foo() = new();

# Need at least one positional argument. Otherwise stack overflow.
function Foo(z :: Integer; kwargs...)
    f = Foo();
    set_kw_args!(f; kwargs...);
    f.z = z;
    return f

function set_common_fields!(f :: Foo)
    f.x = 1;
    f.y = 2;

function set_kw_args!(f :: Foo; kwargs...)
    for kw in kwargs
        setfield!(f, kw[1], kw[2])

julia> Foo(3)
Foo(1, 2, 3)

Common fields in sub-struct

An alternative is to store common fields in a sub-struct. Passing methods through to these fields can be automated using @forward in Lazy.jl.

using Lazy
struct Foo
@forward Foo.x (, Base.isempty)

The tricky part is to modify these fields. One possible solution:

Base.@kwdef mutable struct FooCommon
    x = 1
    y = 2

Base.@kwdef mutable struct Foo
    fc :: FooCommon
    z = 3
    zz = 4

# Again: need at least one positional argument to avoid stack overflow.
function Foo(z; kwargs...)
    f = Foo(fc = FooCommon());
    set_kwargs!(f; kwargs...);
    return f

function set_kwargs!(f :: Foo; kwargs...)
    for kw in kwargs
        set_field!(f, kw[1], kw[2]);

# Note the changed function name. Cannot overload `setfield!`.
function set_field!(f :: Foo, fName :: Symbol, fValue)
    if hasproperty(f, fName)
        setfield!(f, fName, fValue);
        setfield!(f.fc, fName, fValue);

Method Forwarding

Lazy.jl implements @forward to forward a single method to a child object. Example:

struct B end
foo(b :: B) = "B";
struct A
    b :: B
Lazy.@forward A.b foo;
foo(A(B())) == "B";

ReusePatterns.jl defines @forward which forwards all methods to a child object. The idea is to be able to add fields to a struct without losing all of the existing methods.

Parametric Types (1.7)

This works:

struct Foo{T <: Integer} end

This does not:

struct Foo{T <: Integer} end
Foo{7}()  # error

User Defined Types (1.5)

LazyInitializedFields.jl is a nice way of handling partially initialized struct fields.

Properties and fields

getfield is a "built-in" function that always points to a struct field directly. Example:

struct A
  x :: Dict{Symbol,Int}
  y :: Int
a = A(Dict([:a => 1]), 2);
getfield(a, :x) isa Dict{Symbol, Int}

getproperty is generically the same as getfield. But note that propertynames(A) spits out additional hidden properties, not just :x, :y.

Users can overload getproperty to point to something more useful than the direct fields of the struct:

Base.getproperty(a :: A, n :: Symbol) = getfield(a, :x)[n];

Then also overload propertynames so that functions that rely on the public properties of A work.