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; But isa(Foo{Int}, DataType) == true.

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.

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

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
    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. This is no longer maintained. Its readme links to other implementations of composition and inheritance.

Parametric Types (1.7)

This works:

struct Foo{T <: Integer} end
Foo{UInt8}()

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.

AutoPrettyPrinting.jl automatically prints user defined structs in human readable format.

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.