functionmatlab

Handling large number of name/value parameters in MATLAB


I have some MATLAB functions with a large number of optional parameters, that I want to specify as name/value arguments to keep the interfaces clear and avoid the confusion that comes with a large number of positional arguments.

There is a hierarchy of functions, where higher-level functions have all the same arguments as lower-level functions, plus some additional ones (in fact the functions are constructors in a class hierarchy - I don't think that is relevant, but including it here in case it is).

An example would be something like the below. As you can see, there is a lot of repeated code (typically there are dozens of name/value arguments, not just the few shown here, and dozens of different functions). The main problems with this approach are:

  1. I need to repeat the low level arguments in every arguments block, together with their default initializations. I would prefer to only list the arguments in the lower level functions, and have the defaults be set by lower level functions in the case that they are not set by higher level functions.
  2. I need to explicitly name every single argument when calling a lower level function from a higher level function. This leads to a lot of pain if I ever need to e.g. add a new low-level argument, as it requires modifying every single higher level function, and doing this in an untyped language is very error prone!
function highLevelFunc(opts)

    arguments
        opts.highLevelArg_1 = [];
        opts.highLevelArg_2 = [];
        opts.highLevelArg_3 = [];
        opts.midLevelArg_1  = [];
        opts.midLevelArg_2  = [];
        opts.midLevelArg_3  = [];
        opts.lowLevelArg_1  = [];
        opts.lowLevelArg_2  = [];
        opts.lowLevelArg_3  = [];
    end

    midLevelResult = midLevelFunc(...
        midLevelArg_1 = opts.midLevelArg_1, ...
        midLevelArg_2 = opts.midLevelArg_2, ...
        midLevelArg_3 = opts.midLevelArg_3, ...
        lowLevelArg_1 = opts.lowLevelArg_1, ...
        lowLevelArg_2 = opts.lowLevelArg_2, ...
        lowLevelArg_3 = opts.lowLevelArg_3 ...
    );

    % high level code goes here
end

function midLevelFunc(opts)

    arguments
        opts.midLevelArg_1 = [];
        opts.midLevelArg_2 = [];
        opts.midLevelArg_3 = [];
        opts.lowLevelArg_1 = [];
        opts.lowLevelArg_2 = [];
        opts.lowLevelArg_3 = [];
    end

    lowLevelResult = lowLevelFunc(...
        lowLevelArg_1 = opts.lowLevelArg_1, ...
        lowLevelArg_2 = opts.lowLevelArg_2, ...
        lowLevelArg_3 = opts.lowLevelArg_3 ...
    );

    % mid-level code goes here
end

function lowLevelFunc(opts)

    arguments
        opts.lowLevelArg_1 = [];
        opts.lowLevelArg_2 = [];
        opts.lowLevelArg_3 = [];
    end

    % low-level code goes here
end

I hoped that I could use varargin to avoid repeating argument names, but it seems that the language does not allow this if I want to use it in combination with name/value arguments. The code below gives me an error "Positional arguments must be defined before name-value arguments".

function midLevelFunc(opts, varargin)

    arguments
        opts.midLevelArg_1 = [];
        opts.midLevelArg_2 = [];
        opts.midLevelArg_3 = [];
    end

    arguments (Repeating)
        varargin
    end

    lowLevelResult = lowLevelFunc(varargin{:});

    % mid-level code goes here
end

I could put the varargin before the name/value block, but I think this then forces the calling function to put lower-level arguments before higher-level arguments when calling the higher-level function, which feels unintuitive:

function midLevelFunc(varargin, opts)

    arguments (Repeating)
        varargin
    end

    arguments
        opts.midLevelArg_1 = [];
        opts.midLevelArg_2 = [];
        opts.midLevelArg_3 = [];
    end

    lowLevelResult = lowLevelFunc(varargin{:});

    % mid-level code goes here
end

Are there better solutions here?


Solution

  • Between nargin and the arguments syntax came the inputParser which can achieve this through a combination of

    With your example, it would look something like this, where you only have to maintain the lower level name-value pairs in their respective functions

    function highLevelFunc( varargin )
    
        p = inputParser();
        p.addParameter( 'highLevelArg', 'highDefault' );
        p.KeepUnmatched = true;
        p.parse( varargin{:} );
    
        args = namedargs2cell( p.Unmatched );
        midLevelFunc( args{:} );
    
        % high-level code goes here
        highLevelArg = p.Results.highLevelArg;
        fprintf( 'High level arg: %s\n', highLevelArg );
    end
    
    function midLevelFunc( varargin )
    
        p = inputParser();
        p.addParameter( 'midLevelArg', 'midDefault' );
        p.KeepUnmatched = true;
        p.parse( varargin{:} );
    
        args = namedargs2cell( p.Unmatched );
        lowLevelFunc( args{:} );
    
        % mid-level code goes here
        midLevelArg = p.Results.midLevelArg;
        fprintf( 'Mid level arg: %s\n', midLevelArg );
    end
    
    function lowLevelFunc( varargin )
    
        p = inputParser();
        p.addParameter( 'lowLevelArg', 'lowDefault' );
        p.parse( varargin{:} );
    
        % low-level code goes here
        lowLevelArg = p.Results.lowLevelArg;
        fprintf( 'Low level arg: %s\n', lowLevelArg );
    end
    

    Test run where we only overwrite the lowest input:

    >> highLevelFunc( 'lowLevelArg', 'test' )
    Low level arg: test
    Mid level arg: midDefault
    High level arg: highDefault
    

    For your application to super/sub classes, there is some functionality to inherit properties of the superclass as additional name-value pairs using opts.?SuperClass in the arguments block, but if you just want name-value pairs without them being properties I'm not sure that functionality exists. Ref https://mathworks.com/help/matlab/matlab_oop/class-constructor-methods.html#mw_749777e1-ea34-4e8e-b4d3-317d6d61131c