Structuring a Type with Exception Messages

I’m new to struct and have found parametric types a bit difficult to understand. I would like to constrain the admissible types and fail gracefully with intelligible messages.

For instance, let’s say I only want to admit matrices of numbers. Here’s a first pass:

struct NumMatrix{T <: Number, S <: AbstractMatrix{T}}
    A :: S
end
NumMatrix(["a" "b"; "c" "d"])
ERROR: MethodError: no method matching NumMatrix(::Matrix{String})
Closest candidates are:
  NumMatrix(::S) where {T<:Number, S<:AbstractMatrix{T}} at REPL[1]:2

Did I get it right with the T and S?

Now I want to add a customized error message. If I use {T <: Number} inside the type declarations, my error messages will get short cut, so I drop the <: Number part:

struct NumMatrix{T, S <: AbstractMatrix{T}}
    A :: S
    function NumMatrix(A::S) where {T, S <: AbstractMatrix{T}}
        if !(T <: Number) 
            throw(ErrorException("elements of matrix $A must of type Number, but instead are of type $T"))
        end
        new{T,S}(A)
    end
end
NumMatrix(["a" "b"; "c" "d"])
ERROR: elements of matrix ["a" "b"; "c" "d"] must of type Number, but instead are of type String

Or with a more specific error handling (the matrix doesn’t print so pretty, but that’s fine):

struct TypeError <:Exception 
    msg :: String
end
struct NumMatrix{T <: Number, S <: AbstractMatrix{T}}
    A :: S
    function NumMatrix(A::S) where {T, S <: AbstractMatrix{T}}
        if T <: Number
            new{T,S}(A)
        else 
            throw(TypeError("elements of matrix $A must be of type Number, but instead are of type $T"))
        end
    end
end
NumMatrix(["a" "b"; "c" "d"])
ERROR: TypeError("elements of matrix [\"a\" \"b\"; \"c\" \"d\"] must be of type Number, but instead are of type String")

Is this about right?

P.S. In a moment of madness, I also tried this:

struct NumMatrix{T <: Number || throw(ErrorException("No!")), S <: AbstractMatrix{T}}

but that isn’t allowed. Is there any way to throw error messages directly in the struct, without a method? Writing a method for it is fine, but having to re-state where {T, S <: AbstractMatrix{T}} and write new{T,S}(A) is a bit long-winded. Thanks.

You could use the parameters package with something like this:

julia> using Parameters
       @with_kw struct NumMatrix{T}
         A::T
         @assert A isa AbstractMatrix "A must be a matrix"
         @assert eltype(A) <: Number "eltype(A) must be a number" 
       end
NumMatrix

julia> NumMatrix(zeros(2,2))
NumMatrix{Matrix{Float64}}
  A: Array{Float64}((2, 2)) [0.0 0.0; 0.0 0.0]


julia> NumMatrix([1,2])
ERROR: AssertionError: A must be a matrix

julia> NumMatrix([ 'a' 'b' ; 'c' 'd' ])
ERROR: AssertionError: eltype(A) must be a number

1 Like

No. Errors are control flow, so they can only happen in some form of code that is actually running. For types, that’s the constructor.

This is of course subjective, but I think that after a while you may find the default message intelligible enough. IMO duplicating that is a more verbose explanation is redundant.

1 Like

I am not sure the use case here, but I have adapted some error messages like these if the purpose was to expose something more user friendly to users which are not programmers and are just using the package.

As something extra, I many times would like to throw an error message without the stack trace, if the purpose is just to inform a user that some input was not provided correctly.

1 Like

Thanks for your suggestions. So just to briefly reflect on my use case. Essentially yes, as Leandro suggested, the purpose is to have a smoother user experience. If the console shows a message like “The matrix elements must be UInt8. This is done for efficiency. Have a great day.” or something like that, the user will know that the error was part of a plan, whereas the stacktrace error will suggests that it may have been a design error. But it’s true that for personal consumption it’s extra effort that is not warranted.

The reason for asking whether an error handling mechanism could bypass a method was the following. Say I have several functions in my struct and I want them to be integer-valued only. Each function will have its own type, e.g. struct MyStruct{F1,F2,F3,F4,F5}... Say I write a method whose purpose is just to handle that, I will then have to have where {F1, F2, F3, F4, F5} tacked on to the method, which seemed cumbersome (but it’s absolutely fine, I just wondered if there was a way to bypass it). Maybe I can define a variable like F = F1, F2, F3, F4, F5 and use F instead (I haven’t tried, I’m writing from my phone, it’s just a thought in passing).

Leandro: in your answer the type-handling is a lot simpler, you have just T, while I had S<:AbstractMatrix{T}, which is inspired mainly by the docs section on parametric types. But perhaps it’s not worth being so finicky?

Thanks a lot Leandro and Tamas.

That is most about function, I think. For example, will you accept vectors there? Is there any reason to explicitly exclude other types? (Of course your checks would have to be adapted accordingly).

1 Like

This is difficult to do for generic code where the same function may be reused for various purposes at different levels in the stack. The matrix in question may be something the user provided, or an interim result which just happened to be of a different type because of a promotion issue, etc.

In some contexts, validating input and failing early with an informative message may be an option. Otherwise, you can catch some errors and rethrow a more informative message for some conditions, and signal that it is a bug to be reported for the rest.

Please reserve use of @assert for statements that are true, not things that may be false. @assert is useful for reasoning about the state whether the @assert has executed or not – I like being able to know that in the line before the @assert it is still true.

An assertion violation indicates a bug in your code, not in the caller’s code.

An even stronger position is The Use Of assert() In SQLite, but I wouldn’t go that far in Julia.

1 Like