Declaring a structure as a subtype?

So I have an algorithm that has several different steps to perform on matrices. I started declaring new types to ensure that matrices aren’t passed prematurely to the next step, or redundantly to a previous one.

For example, the function for step 1 might accept any old matrix,

function step_1_foo(M::Matrix)

but step 2 might want a matrix that is specifically in reduced row echelon form. For this I declared a structure that has one field that’s a sparse matrix.

struct SparseRREF
    R::SparseMatrixCSC
end

I then would have step_1_foo return type SparseRREF and make the next function take SparseRREF as its argument.

function step_2_foo(S::SparseRREF)
    M = S.R
    # etc...
end

This is a little annoying because when I want to use the sparse matrix, I have to grab it from the struct first. Which leads me to my question.

Is there a better way of declaring these types? I don’t really want to add any functionality to the structure, other than to specify that it has been processed by a certain step. I don’t want to make an alias, because I don’t want any Matrix to be seen accepted as a SparseRREF type. Could I declare it as a type that inherits Matrix?

This looks fine to me, and may be the solution with the lowest cost: just one extra line in each function.

There is no mechanism to automatically forward all methods to a field (it’s not really a good idea, you can find many discussions about this). The best strategy is to keep APIs small, and then just forward the relevant methods.

FWIW, I think that using wrappers just to ensure the right order in what is otherwise not exposed library code (ie an implementation of an algorithm inside the package) may be overkill. I would just rely on unit tests to make sure the code is correct. Of course if the steps of the algorithm are exposed then this may be way to do it.

3 Likes

Thanks!

You may be right that it’s overkill, but since I’m coding all by my lonesome (with the exception of you fine folks :slightly_smiling_face:) and don’t really plan on distributing my code, I like having the extra protection to make sure I’m not screwing up.

If only step is needed, then you can declare one type instead of many, and write one extra line in each function like this:

struct MatrixWithStep{T,S}
    mat::T
    step::S
end

@inline MatrixWithStep(x, s::Int) = MatrixWithStep(x, Val{s}())

@inline function makesure(x::MatrixWithStep{T,S}, ::S) where {T,S}
    x.mat
end

@inline makesure(x, s::Int) = makesure(x, Val{s}())

function step1(x)
    MatrixWithStep(x.+1, 1)
end

function step2(x)
    m = makesure(x, 1)
    MatrixWithStep(m.+2, 2)
end

function step3(x)
    m = makesure(x, 2)
    MatrixWithStep(m.+3, 3)
end

Things like @inline and Val{s}() is to make sure that things like makesure can be compiled away when using in the correct order so there will be no performance penalty after compilation. For example, if we further have

f1(x) = step2(step1(x))
f2(x) = step3(step1(x))

then makesure check in both f1 and f2 will be compiled away:

julia> @code_warntype(f1(1))
Body::MatrixWithStep{Int64,Val{2}}
1 ──       goto #3 if not true
2 ──       nothing
3 ┄─ %3  = (Base.add_int)(x, 1)::Int64
└───       goto #4
4 ──       goto #5
5 ──       goto #6
6 ──       goto #7
7 ──       goto #9 if not true
8 ──       nothing
9 ┄─ %10 = (Base.add_int)(%3, 2)::Int64
└───       goto #10
10 ─       goto #11
11 ─       goto #12
12 ─ %14 = %new(MatrixWithStep{Int64,Val{2}}, %10, $(QuoteNode(Val{2}())))::MatrixWithStep{Int64,Val{2}}
└───       goto #13
13 ─       return %14

julia> @code_warntype(f2(1))
Body::Union{}
1 ─      goto #3 if not true
2 ─      nothing
3 ┄ %3 = (Base.add_int)(x, 1)::Int64
└──      goto #4
4 ─      goto #5
5 ─      goto #6
6 ─ %7 = %new(MatrixWithStep{Int64,Val{1}}, %3, $(QuoteNode(Val{1}())))::MatrixWithStep{Int64,Val{1}}
└──      goto #7
7 ─      invoke Main.step3(%7::MatrixWithStep{Int64,Val{1}})
└──      $(Expr(:unreachable))
2 Likes

That’s a cool idea I hasn’t considered. It’s not the best choice for my particular situation, though, because my steps aren’t necessarily ordered. This reminds me a lot of enum in Java. Does Julia have any sort of enum equivalent?

Yes, julia has enum, see https://docs.julialang.org/en/v1/base/base/#Base.Enums.@enum.
And you can also use symbols like :step1, :step2, :stepsparse in my example instead of Int if the steps are not ordered.

2 Likes