Search code examples
matlabvariadic-functionsargument-validation

Does cross-argument function argument validation not work with (Repeating) arguments?


Summary

When using function argument validation that depends on multiple arguments in a (Repeating) arguments block, the current argument is passed to the validation function normally while other arguments are passed as partially-populated cell arrays. This contasts with how things work in non-(Repeating) arguments blocks. Is this the expected behavior or a bug?

Scenario

Consider the function dummy1 below, which uses the custom argument validation function mustBeEqualSize to ensure that the arguments x and y have the same size. Since the validation of y depends on the value of x, I'm calling this "cross-argument" validation*.

*If there's a better term for this, please comment or edit.

function dummy1(x, y)
    arguments
       x (1,:) 
       y (1,:) {mustBeEqualSize(x,y)}
    end
    % Do something with x and y.
end

function mustBeEqualSize(a, b)
    % Validates that function arguments have the same size.
    if ~isequal(size(a), size(b))
        eid = 'Size:notEqual';
        msg = "Arguments must have the same size.";
        throwAsCaller(MException(eid, msg))
    end
end

As written, this form of argument validation works as expected:

dummy1(1:3, 4:6)  % arguments have same size; validation passes (okay)
dummy1(1:3, 4:7)  % arguments have different size; validation fails (okay)

In both cases, when mustBeEqualSize is called during argument validation for y, both a and b are received as 1xN double arrays, which matches my expectations:

% Inside call to mustBeEqualSize(x,y), when x=1:3, y=4:6 in dummy1
a =
     1     2     3
b =
     4     5     6

The problem arises when dummy1 is modified to accept repeating arguments by adding (Repeating) to the arguments block:

function dummy2(x, y)
    arguments (Repeating)
       x (1,:) 
       y (1,:) {mustBeEqualSize(x,y)}
    end
    % Do something with each pair of x-y arguments.
    % In this body, both x and y will be 1xN cell arrays, where N is the
    % number of argument groups passed.
end

Now when we call dummy2(1:3, 4:6), argument validation fails. Using the debugger, I found that when mustBeEqualSize is called during the validation of y, a is received as a 1x1 cell array while b remains a 1x3 double array:

% Inside call to mustBeEqualSize(x,y), when x=1:3, y=4:6 in dummy2
a =
  1×1 cell array
    {[1 2 3]} 
b =
     4     5     6

The issue is even more obvious when more repeating argument are used:

dummy2(1:3, 4:6, 1:3, 4:6, 1:3, 4:6)  % 3 argument groups

results in

a =
  1×3 cell array
    {[1 2 3]}    {0×0 double}    {0×0 double}
b =
     4     5     6

It seems that during the validation of y in dummy2, y takes on the value of the current argument being validated while x is a (parially populated) cell array buffer that MATLAB allocated to hold all of the x-arguments that were passed.

This of course breaks the cross-argument validation, since only the argument currently being validated actually presents itself as a single argument, while other arguments present themsevles as cell array buffers.

Question

Is the mismatch between how cross-argument validation works for (Repeating) versus non-(Repeating) arguments a bug, or is cross-argument validation with (Repeating) arguments not supported in MATLAB? If this difference in behavior is expected, is there any way to make cross-argument validation work with (Repeating) arguments?

The MATLAB docs say the following about argument validation with Repeating arguments

In the function, each repeating argument becomes a cell array with the number of elements equal to the number of repeats passed in the function call. The validation is applied to each element of the cell array.

which doesn't seem to shed any light on how cross-argument validation should work with repeating arguments.

Tested using MATLAB R2021a (9.10.0.1602886).


Solution

  • Note: This was an edge case of the design we didn't consider. I've made the relevant devs teams aware and they'll consider fixing it in a future release.

    In the short term, you could try grabbing the last non 0x0 double element of the cell array:

    function mustBeEqualSize(x,y)
        if iscell(x)
            % Get last non-empty element of `x`
            notEmptyDouble = @(e)isa(e,'double') && ~isequal(size(e), [0, 0]);
            idx = find(cellfun(notEmptyDouble, x), 1, 'last');
            x = x{idx};
        end
    
        % check equality
        if ~isequal(size(x), size(y))
            error("size mismatch")
        end
    end
    

    This will work on all non-empty arrays, but unfortunately given the empty type used, will not work for dummy2([],[]).

    Also, I want to note, that whatever validator ends up working for this use case, consider if it will break the function if/when this issue is fixed. I.e. if this function needs to handle both double arrays and cell arrays, could be problematic in the future. However if this function only needs double arrays, it can be constrained using

    arguments(Repeating)
        a (1,:) double
        b (1,:) double {mustBeEqualSize(x,y)}
    end
    

    This combined with the iscell check in the validator, should future-proof this function.