Argument checking in user defined types

Hi.
Sorry if this is obvious or belies a complete misunderstanding of the Julia way… but I was trying to figure out how to do argument checking in a user defined type. For example, to enforce that a value be a positive valued number.

As a relative beginner (a Matlab convert) I didn’t really know how to do this. The closest I got in searching the documentation was that it should be done as a constructor, so I need some kind of function in my user defined type.

I managed to find an example from Distributions.jl where you define a normal distribution, where sigma has to be positive

struct Normal{T<:Real} <: ContinuousUnivariateDistribution
    μ::T
    σ::T

    Normal{T}(μ, σ) where {T} = (@check_args(Normal, σ > zero(σ)); new{T}(μ, σ))
end

So this is pretty opaque to me as a beginner, and it seems like @check_args is a macro specific to the Distributions.jl package?

I also found that there is the ArgCheck.jl package which looks promising. But my hunch is that there should be a way of doing this with base Julia.

Anyway, I guess my friendly point here is that as a beginner approaching this pretty routine task I didn’t really get very far. If anyone could point me towards an example, or provide an answer, that would be cool. My hunch is that quite a lot of newcomers to Julia would probably appreciate a little worked example in the docs.

1 Like

There’s an example in the manual which constructs an ordered pair, subject to the constraint that the pair is, in fact, ordered: Constructors · The Julia Language

Does that help?

1 Like

Ah ha! Thanks.

So as a beginner I could not mentally parse the a ? b : c control flow, so it just looked very odd.

But yes, now I figured that out, it looks pretty easy. So you can do something like this

struct MyThing
    whatever::Number
    positive_thing::Number
    
    MyThing(whatever, positive_thing) = positive_thing > 0 ?  new(whatever, positive_thing) : error("positive_thing should be positive")
end

That seems fine, but be careful because

struct MyThing
    whatever::Number
    positive_thing::Number

is not strictly typed. A better approach is either

struct MyThing{A,B}
    whatever::A
    positive_thing::B

or if you know the types of the things, you can do it directly

struct MyThing
    whatever::Float64
    positive_thing::Float64

(although first option is more general and scalable to different types)

2 Likes

Yes indeed.

The ternary a ? b : c syntax is kind of weird if you’re not used to it from a language like C. I personally find the more verbose version clearer in this case:

struct MyThing
  positive_thing::Number
  function MyThing(positive_thing)
    if positive_thing > 0
      return new(positive_thing)
    else
      error("Should be positive")
    end
  end
end

although as @Datseris says, using MyThing{T <: Number}; positive_thing::T will generally perform better. See: https://docs.julialang.org/en/stable/manual/performance-tips/#Avoid-fields-with-abstract-type-1

I agree, I also find that clearer. So a few things going on there…

  1. Is that validation function automatically run (not simply defined) because it is declared in a struct?
  2. Your suggestion is to be more specific and essentially create a new PositiveNumber type so you are dealing with one field at a time, then you can enforce that some of your types in other larger composition types are ::PositiveNumber ? Or was that just a simplification in your example?

For the sake of learning, I checked and it looks like I can do it in one…

struct MyThing
  whatever::Float64
  positive_thing::Float64
  function MyThing(whatever, positive_thing)
    if positive_thing > 0
      return new(whatever, positive_thing)
    else
      error("positive_thing should be positive")
    end
  end
end
  1. That function is an “inner constructor” (see Constructors · The Julia Language ), so it’s the lowest-level way to construct a MyThing. I’m just using the syntax:
function foo(x, y)
  x + y
end

instead of the shorter but exactly equivalent syntax:

foo(x, y) = x + y

which is used in that particular example in the manual.

  1. I was just simplifying. But having a PositiveNumber{T <: Number} type that does all your positivity-checking would also be a reasonable thing to do.
2 Likes