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:
- Possible type instability as the compiler tries to figure out the types of the custom type fields.
- 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.