Can a julia struct be defined with persistent requirements on field dimensions?

Viewed 124

If I define a new struct as

mutable struct myStruct
   data::AbstractMatrix
   labels::Vector{String}
end

and I want to throw an error if the length of labels is not equal to the number of columns of data, I know that I can write a constructor that enforces this condition like

myStruct(data, labels) = length(labels) != size(data)[2] ? error("Labels incorrect length") : new(data,labels)

However, once the struct is initialized, the labels field can be set to the incorrect length:

m = myStruct(randn(2,2), ["a", "b"])
m.labels = ["a"]

Is there a way to throw an error if the labels field is ever set to length not equal to the number of columns in data?

2 Answers

You could use StaticArrays.jl to fix the matrix and vector's sizes to begin with:

using StaticArrays

mutable struct MatVec{R, C, RC, VT, MT}
   data::MMatrix{R, C, MT, RC}  # RC should be R*C
   labels::MVector{C, VT}
end

but there's the downside of having to compile for every concrete type with a unique permutation of type parameters R,C,MT,VT. StaticArrays also does not scale as well as normal Arrays.

If you don't restrict dimensions in the type parameters (with all those downsides) and want to throw an error at runtime, you got good and bad news.

The good news is you can control whatever mutation happens to your type. m.labels = v would call the method setproperty!(object::myStruct, name::Symbol, v), which you can define with all the safeguards you like.

The bad news is that you can't control mutation to the fields' types. push!(m.labels, 1) mutates in the push!(a::Vector{T}, item) method. The myStruct instance itself doesn't actually change; it still points to the same Vector. If you can't guarantee that you won't do something like x = m.labels; push!(x, "whoops") , then you really do need runtime checks, like iscorrect(m::myStruct) = length(m.labels) == size(m.data)[2]

A good option is to not access the fields of your struct directly. Instead, do it using a function. Eg:

mutable struct MyStruct
   data::AbstractMatrix
   labels::Vector{String}
end

function modify_labels(s::MyStruct, new_labels::Vector{String})
    # do all checks and modifications
end  

You should check chapter 8 from "Hands-On Design Patterns and Best Practices with Julia: Proven solutions to common problems in software design for Julia 1.x"

Related