Skip to content

Types

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
end

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.

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)))
        end
    end
end

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

struct Foo
    @commonfields
    z
end

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
end
@c1 (macro with 1 method)

julia> struct Foo
          @c1
          z :: String
       end

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

Common fields with default values

mutable struct Foo
    x
    y
    z

    Foo() = new();
end

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


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

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

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
  x
end
@forward Foo.x (Base.show, Base.isempty)

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

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

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

# 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
end

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

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

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
end
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.

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
end
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.