Protect Entry of a Struct

Hello!

Suppose I have this simple example where I use the Parameters package:

using Parameters

@with_kw struct MyTest
       a = 1
       b = 2*a
 end

Note how b is calculated based on a. If I write:

MyTest(a=4)
MyTest
  a: Int64 4
  b: Int64 8

Which is as expected. On the other hand if I write:

MyTest(b=5)
MyTest
  a: Int64 1
  b: Int64 5

In which case I have overwritten b = 2*a. Now in some instances this can be useful and so on, but my question is then:

Is it possible to in some way PROTECT a specific entry in a struct? To ensure that it cannot be set explicitly?

Or atleast output a warning; “WARNING: MyTest.b was set manually.”

Kind regards

The @with_kw macro just adds convenience constructors that take kwargs. You can always just define them yourself:

struct MyTest
    a
    b
    MyTest(;a = 1) = new(a, 2*a)
end
1 Like

I really like the @with_kw approach and personally also like the ability to overwrite, but I would just like to cast a warning if it has been overwritten.

Thanks for showing me that approach though, I just think it might become convulated qutie fast if say I had 5 entries which had to be calculated in my struct.

Kind regards

I think this does what you want:

julia> @with_kw struct MyTest
         a = 1
         b = 2*a
         function MyTest(a,b)
           @assert b == 2*a
           new(a,b)
         end
       end

julia> MyTest(a=1,b=3)
ERROR: AssertionError: b == 2a

julia> MyTest(b=3)
ERROR: AssertionError: b == 2a

julia> MyTest(a=1,b=2)
MyTest
  a: Int64 1
  b: Int64 2

julia> MyTest(b=2)
MyTest
  a: Int64 1
  b: Int64 2

julia> MyTest(a=3)
MyTest
  a: Int64 3
  b: Int64 6




Looks like Parameters.jl supports putting those @asserts directly in the field definition itself — you needn’t create the constructors yourself. I’d guess something like this would do the ticket:

@with_kw struct MyTest
    a = 1
    b = 2*a
    @assert b == 2a
 end

https://mauro3.github.io/Parameters.jl/stable/manual/#Types-with-default-values-and-keyword-constructors

2 Likes

Just to address the specific wording here even though I know it’s not really what you mean: No. It is fundamentally impossible in julia to strictly enforce constraints on the values of the fields in a struct.

The best one can do is use inner constructors to enforce constraints, but those can be bypassed in various ways. For instance:

julia> using Parameters

julia> @with_kw struct MyTest{T}
           a::T = 1
           b::T = 2*a
           @assert b == 2a
       end
MyTest

julia> MyTest(a=1, b=1)
ERROR: AssertionError: b == 2a

julia> reinterpret(MyTest{Int}, [1,  1])[1]
MyTest{Int64}
  a: Int64 1
  b: Int64 1

A determined user can always find a way to stick an ‘invalid’ entry into a struct (so long as the type is compatible). The best you can do is make it inconvenient to do so and then tell users that if they bypass your constructors, it’s their own fault if things go wrong.

2 Likes

@lmiq @mbauman

Thanks for your code pieces! It is not exactly what I want, but it is atleast a possible route to go.

@Mason

Most users are not going to figure out the reinterpret trick which you did and I was completely oblivious of it also to be honest - so great find!

Regarding the enforcing of constraints, would it be possible to any way write an @assert expression which basically is a warning? So for example if “b” is set externally and not defined through “a”, then it would simply allow it (because it is not possible to disallow as you say), but print a warning saying that the automatic calculation of this parameter was bypassed.

For my use case the exact value of a or b, is not so important it is just whether or not it was overwritten or calculated based on a predefined expression.

Basically something like I mentioned in the first post; “WARNING: MyTest.b was set manually.” But I would want this for multiple variables, imagine if I had also fields c,d,e,f and so forth.

Kind regards

In the custom construct you can just remove the assert and add a warning.

julia> @with_kw struct MyTest
         a = 1
         b = 2*a
         function MyTest(a,b)
           b == 2*a || println("Warning: b ≠ 2a")
           new(a,b)
         end
       end
2 Likes

No, the reinterpret trick (or other tricks) will also allow one to bypass any automatic warning mechanism. If julia had a mechanism to strictly enforce this stuff, a lot of important optimizations would be impossible.

Don’t worry about the points I’m bringing up here though, it’s most likely not relevant for you. Just stick warnings or assertions in your constructors and you’ll be fine. If a user bypasses your constructors, that’s their fault.

1 Like

@lmiq

Thanks for the suggestions!

@Mason

Unfortunate, but thanks for the clarification. I personally would like this kind of feature, but I see why it might be difficult to implement etc. Thanks for spending time on the question!

Kind regards

My understanding is that the problem is not that it’s difficult to implement, it’s that the very concept is anathema to julia being usable as a high performance language.

We need these sorts of low level escape hatches for various things. But the upside is that it is considered the user of that escape hatches responsibility to make sure anything they do there is valid. So again, this is not really your problem.

Just a nitpick: Better to do @warn "b ≠ 2a" to hook into all the existing logging features.

4 Likes