Class property validation failure due to implicit instantiation defaults.

40 views (last 30 days)
Say I have
classdef MyClass
properties
a (1, 1) {mustBePositive}
end
methods
function obj = MyClass(a)
obj.a = a;
end
end
end
which will produce the following error "Value must be positive" upon running
MyClass(1)
which is ridiculous since 1 is clearly positive.
Something about Matlab initializing the class instance with implicit default 0 I think? I don't understand why Matlab doesn't just allocate space dependent on the conditions provided (e.g. "mustBePositive") like other programming languages do.
So, time to fix it. If I change my class to
classdef MyClass
properties
a (1, 1) {mustBePositive} = []
end
methods
function obj = MyClass(a)
obj.a = a;
end
end
end
then the property validation fails because it must be a scalar. I'm considering writing my own "mustBePositive" function, but someone somewhere is going to tell me off for bad programming practice, of which I agree.
If I instead change my class to
classdef MyClass
properties
a (1, 1) {mustBePositive} = 1
end
methods
function obj = MyClass(a)
obj.a = a;
end
end
end
then sure, it doesn't throw an error. But this, I would argue, is bad implementation practice, especially in the context that I'm applying Matlab classes for (which I won't go into). If I am to have a default value, then it must be acceptably applicable in the contexts of usage, which it is not.
How can I deal with this property validation failure for implicit defaults?
  1 Comment
per isakson
per isakson on 21 Jan 2022
Edited: per isakson on 21 Jan 2022
Yes, that's the way Matlab works.
The documentations, Validate Property Values, says:
Specify Valid Default
Ensure that any default value [including implicit defaults] assigned to the property meets the restrictions imposed by the specified validation.
I don't think there is a better way than explicitely assigning a valid default value.

Sign in to comment.

Answers (2)

Lawrence
Lawrence on 19 Apr 2022
Edited: Lawrence on 19 Apr 2022
You can hack your way around this requirement by making the verified property dependent and having a private unverified implementation.
classdef MyClass
properties (Dependent = true)
a {mustBePositive}
end
properties (Access = private)
a_impl
end
methods
function obj = MyClass(a)
obj.a = a;
end
function obj = set.a(obj,new_a)
obj.a_impl = new_a;
end
function a = get.a(obj)
a = obj.a_impl;
end
end
end
With dependent values there are no default values assigned, so you don't get the pre-constructor initialization error. The validation still is applied when you try to set obj.a, and the unqualified use of the "a" input in the MyClass constructor ensures that it will be passed and the initial value will be verified. Note that this class is still not default-constructable, since I haven't provided a default value of the "a" input into the MyClass constructor.
  3 Comments
Lawrence
Lawrence on 21 Apr 2022
Edited: Lawrence on 21 Apr 2022
Edit: Ignore the following paragraph about dynamic properties, since apparently they can't use validation the same way static properties can.
If you're working with a handle class, then you can metaprogram a solution using dynamicprops https://www.mathworks.com/help/matlab/ref/dynamicprops-class.html. Each dependent property would be dynamic. You can lessen the work somewhat by replacing the private implementations with a single private struct to hold values in fields that match the variable you're setting, combined with a generic get/set_dependent_field function which you can call in each dynamic dependent property's get and set methods.
Sadly, I can't think of any easy solutions if you're working with a value class. You can still use the single private struct and generic get/set functions I mentioned above to make the work a little less painful.
Lawrence
Lawrence on 21 Apr 2022
Edited: Lawrence on 21 Apr 2022
If you're willing to dive deep into hacky solutions to simplify the work of implementing this for many properties, you can try overloading subsasgn to interrupt assignement to the dependent properties. Then you can do the redirect to private struct and/or private implementations, making use of dynamic field references https://www.mathworks.com/help/matlab/matlab_prog/generate-field-names-from-variables.html. Note that this bypasses any property block function argument validation for the dependent properties. So, you need to call the validation functions manually in subsasgn before assigning into the private struct.
Personally, around this point I myself would just throw in the towel and supply valid default values.

Sign in to comment.


Lawrence
Lawrence on 21 Apr 2022
Edited: Lawrence on 21 Apr 2022
I read your comment about needing to do this for many properties. After doing a little more digging, I learned that you can get the meta.Validation information for any property and call that at will, which lets you combine overloading subsasgn with typical properties-block validation specification. This lets you meta-program a way to simply specify the dependent properties. It's arguably even more of a hack than the previous answer I gave above, but at least it's using the facilities that MATLAB designed for this type of hacking.
This answer also properly errors if you try to access an uninitialized variable, rather than return the empty double array that the private implementation in my previous answer would return regardless of if an empty double passes validation. (I suppose, though, that in the previous version you could use a struct and verify the field exists or use metaprogramming to validate on get as well to restore this behavior. Perhaps this would even be desirable, since another programmer could come along and monkey with the private implementations and break the validation you have specified.)
One downside of this method that you have no get and set methods for the dependent properties, so you cannot see them when displayed in the command window or interact with them in the variable editor panel. You can fix command window display with https://www.mathworks.com/help/matlab/matlab_oop/implementing-a-custom-display.html, but I'm not sure yet how to fix the variable editor.
Another downside is that subsasgn isn't called automatically inside the constructor, so you have to either call it manually (to get the validation) or set to the DependentDataStore directly (simpler but no validation).
classdef MyClass
properties (Dependent = true)
a {mustBePositive}
b {mustBeTextScalar}
end
properties (Access = private)
DependentDataStore (1,1) struct = struct
end
methods
function this = h(a,b)
%Either this (the "proper" way):
this = subsasgn(this,struct('type','.','subs','a'),a);
this = subsasgn(this,struct('type','.','subs','b'),b);
%Or this (simpler but with no validation):
this.DependentDataStore.a = a;
this.DependentDataStore.b = b;
%But not this (errors b/c there are no set methods):
% this.a = a;
% this.b = b;
end
function B = subsref(A,S)
meta = getPropertyMeta(A,S);
if meta.Dependent
if ~isfield(A.DependentDataStore,S(1).subs)
ME = MException('DependentProperty:UninitializedUse','The value of property %s has not been initialized and therefore cannot be read.',S(1).subs);
throwAsCaller(ME);
end
if numel(S) == 1
B = A.DependentDataStore.(S(1).subs);
else
B = subsref(A.DependentDataStore.(S(1).subs),S(2:end));
end
else
B = builtin('subsref',A,S);
end
end
function A = subsasgn(A,S,B)
meta = getPropertyMeta(A,S);
if meta.Dependent
newVal = calculateValueAfterAssignment(A,S,B);
try
newVal = validateNewVal(meta,newVal);
catch ME
throwAsCaller(ME)
end
A.DependentDataStore.(S(1).subs) = newVal;
else
A = builtin('subsasgn',A,S,B);
end
end
end
end
function meta = getPropertyMeta(A,S)
meta = metaclass(A);
meta = meta.PropertyList(strcmp({meta.PropertyList.Name},S(1).subs));
end
function newVal = validateNewVal(meta,newVal)
if ~isempty(meta.Validation)
newVal = meta.Validation.validateValue(newVal);
end
end
function newVal = calculateValueAfterAssignment(A,S,B)
if numel(S) == 1
newVal = B;
else
if ~isfield(A.DependentDataStore,S(1).subs)
currentVal = [];
else
currentVal = A.DependentDataStore.(S(1).subs);
end
newVal = subsasgn(currentVal,S(2:end),B);
end
end
And example usage:
>> obj = MyClass(2,'3')
obj =
MyClass with no properties.
>> obj.a = 3
obj =
MyClass with no properties.
>> obj.a
ans =
3
>> obj.a = -2
Value must be positive.
>> obj.b = 'qwer';
>> obj.b
ans =
'qwer'
>> obj.b(2) = 'a'
H =
MyClass with no properties.
>> obj.b
ans =
'qaer'
  1 Comment
Aaron Kaw
Aaron Kaw on 10 May 2022
Thank you so much!
This has become so much more of a hassle than is really needed, and I blame the design of MATLAB. This is one of many issues that I have with MATLAB.
Other languages you simply designate a default value, and you don't go through all this nonsense.
If I ever really REALLY need to implement this, then I'll take a closer look.
Again, thank you for showing me how this could all be done.

Sign in to comment.

Categories

Find more on Argument Definitions in Help Center and File Exchange

Products


Release

R2021b

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!