How to deal with name/value pairs of function arguments in MATLAB

后端 未结 14 1024
忘掉有多难
忘掉有多难 2020-11-28 19:28

I have a function that takes optional arguments as name/value pairs.

function example(varargin)
% Lots of set up stuff
vargs = varargin;
nargs = length(vargs         


        
相关标签:
14条回答
  • 2020-11-28 19:54

    I could yack for hours about this, but still don't have a good gestalt view of general Matlab signature handling. But here's a couple pieces of advice.

    First, take a laissez faire approach to validating input types. Trust the caller. If you really want strong type testing, you want a static language like Java. Try to enforce type safety every where in Matlab, and you'll end up with a good part of your LOC and execution time devoted to run time type tests and coercion in userland, which trades in a lot of the power and development speed of Matlab. I learned this the hard way.

    For API signatures (functions intended to be called from other functions, instead of from the command lines), consider using a single Args argument instead of varargin. Then it can be passed around between multiple arguments without having to convert it to and from a comma-separated list for varargin signatures. Structs, like Jonas says, are very convenient. There's also a nice isomorphism between structs and n-by-2 {name,value;...} cells, and you could set up a couple functions to convert between them inside your functions to whichever it wants to use internally.

    function example(args)
    %EXAMPLE
    %
    % Where args is a struct or {name,val;...} cell array
    

    Whether you use inputParser or roll your own name/val parser like these other fine examples, package it up in a separate standard function that you'll call from the top of your functions that have name/val signatures. Have it accept the default value list in a data structure that's convenient to write out, and your arg-parsing calls will look sort of like function signature declarations, which helps readability, and avoid copy-and-paste boilerplate code.

    Here's what the parsing calls could look like.

    function out = my_example_function(varargin)
    %MY_EXAMPLE_FUNCTION Example function 
    
    % No type handling
    args = parsemyargs(varargin, {
        'Stations'  {'ORD','SFO','LGA'}
        'Reading'   'Min Temp'
        'FromDate'  '1/1/2000'
        'ToDate'    today
        'Units'     'deg. C'
        });
    fprintf('\nArgs:\n');
    disp(args);
    
    % With type handling
    typed_args = parsemyargs(varargin, {
        'Stations'  {'ORD','SFO','LGA'}     'cellstr'
        'Reading'   'Min Temp'              []
        'FromDate'  '1/1/2000'              'datenum'
        'ToDate'    today                   'datenum'
        'Units'     'deg. C'                []
        });
    fprintf('\nWith type handling:\n');
    disp(typed_args);
    
    % And now in your function body, you just reference stuff like
    % args.Stations
    % args.FromDate
    

    And here's a function to implement the name/val parsing that way. You could hollow it out and replace it with inputParser, your own type conventions, etc. I think the n-by-2 cell convention makes for nicely readable source code; consider keeping that. Structs are typically more convenient to deal with in the receiving code, but the n-by-2 cells are more convenient to construct using expressions and literals. (Structs require the ",..." continuation at each line, and guarding cell values from expanding to nonscalar structs.)

    function out = parsemyargs(args, defaults)
    %PARSEMYARGS Arg parser helper
    %
    % out = parsemyargs(Args, Defaults)
    %
    % Parses name/value argument pairs.
    %
    % Args is what you pass your varargin in to. It may be
    %
    % ArgTypes is a list of argument names, default values, and optionally
    % argument types for the inputs. It is an n-by-1, n-by-2 or n-by-3 cell in one
    % of these forms forms:
    %   { Name; ... }
    %   { Name, DefaultValue; ... }
    %   { Name, DefaultValue, Type; ... }
    % You may also pass a struct, which is converted to the first form, or a
    % cell row vector containing name/value pairs as 
    %   { Name,DefaultValue, Name,DefaultValue,... }
    % Row vectors are only supported because it's unambiguous when the 2-d form
    % has at most 3 columns. If there were more columns possible, I think you'd
    % have to require the 2-d form because 4-element long vectors would be
    % ambiguous as to whether they were on record, or two records with two
    % columns omitted.
    %
    % Returns struct.
    %
    % This is slow - don't use name/value signatures functions that will called
    % in tight loops.
    
    args = structify(args);
    defaults = parse_defaults(defaults);
    
    % You could normalize case if you want to. I recommend you don't; it's a runtime cost
    % and just one more potential source of inconsistency.
    %[args,defaults] = normalize_case_somehow(args, defaults);
    
    out = merge_args(args, defaults);
    
    %%
    function out = parse_defaults(x)
    %PARSE_DEFAULTS Parse the default arg spec structure
    %
    % Returns n-by-3 cellrec in form {Name,DefaultValue,Type;...}.
    
    if isstruct(x)
        if ~isscalar(x)
            error('struct defaults must be scalar');
        end
        x = [fieldnames(s) struct2cell(s)];
    end
    if ~iscell(x)
        error('invalid defaults');
    end
    
    % Allow {name,val, name,val,...} row vectors
    % Does not work for the general case of >3 columns in the 2-d form!
    if size(x,1) == 1 && size(x,2) > 3
        x = reshape(x, [numel(x)/2 2]);
    end
    
    % Fill in omitted columns
    if size(x,2) < 2
        x(:,2) = {[]}; % Make everything default to value []
    end
    if size(x,2) < 3
        x(:,3) = {[]}; % No default type conversion
    end
    
    out = x;
    
    %%
    function out = structify(x)
    %STRUCTIFY Convert a struct or name/value list or record list to struct
    
    if isempty(x)
        out = struct;
    elseif iscell(x)
        % Cells can be {name,val;...} or {name,val,...}
        if (size(x,1) == 1) && size(x,2) > 2
            % Reshape {name,val, name,val, ... } list to {name,val; ... }
            x = reshape(x, [2 numel(x)/2]);
        end
        if size(x,2) ~= 2
            error('Invalid args: cells must be n-by-2 {name,val;...} or vector {name,val,...} list');
        end
    
        % Convert {name,val, name,val, ...} list to struct
        if ~iscellstr(x(:,1))
            error('Invalid names in name/val argument list');
        end
        % Little trick for building structs from name/vals
        % This protects cellstr arguments from expanding into nonscalar structs
        x(:,2) = num2cell(x(:,2)); 
        x = x';
        x = x(:);
        out = struct(x{:});
    elseif isstruct(x)
        if ~isscalar(x)
            error('struct args must be scalar');
        end
        out = x;
    end
    
    %%
    function out = merge_args(args, defaults)
    
    out = structify(defaults(:,[1 2]));
    % Apply user arguments
    % You could normalize case if you wanted, but I avoid it because it's a
    % runtime cost and one more chance for inconsistency.
    names = fieldnames(args);
    for i = 1:numel(names)
        out.(names{i}) = args.(names{i});
    end
    % Check and convert types
    for i = 1:size(defaults,1)
        [name,defaultVal,type] = defaults{i,:};
        if ~isempty(type)
            out.(name) = needa(type, out.(name), type);
        end
    end
    
    %%
    function out = needa(type, value, name)
    %NEEDA Check that a value is of a given type, and convert if needed
    %
    % out = needa(type, value)
    
    % HACK to support common 'pseudotypes' that aren't real Matlab types
    switch type
        case 'cellstr'
            isThatType = iscellstr(value);
        case 'datenum'
            isThatType = isnumeric(value);
        otherwise
            isThatType = isa(value, type);
    end
    
    if isThatType
        out = value;
    else
        % Here you can auto-convert if you're feeling brave. Assumes that the
        % conversion constructor form of all type names works.
        % Unfortunately this ends up with bad results if you try converting
        % between string and number (you get Unicode encoding/decoding). Use
        % at your discretion.
        % If you don't want to try autoconverting, just throw an error instead,
        % with:
        % error('Argument %s must be a %s; got a %s', name, type, class(value));
        try
            out = feval(type, value);
        catch err
            error('Failed converting argument %s from %s to %s: %s',...
                name, class(value), type, err.message);
        end
    end
    

    It is so unfortunate that strings and datenums are not first-class types in Matlab.

    0 讨论(0)
  • 2020-11-28 20:02

    Since ages I am using process_options.m. It is stable, easy to use and has been included in various matlab frameworks. Don't know anything about performance though – might be that there are faster implementations.

    Feature I like most with process_options is the unused_args return value, that can be used to split input args in groups of args for, e.g., subprocesses.

    And you can easily define default values.

    Most importantly: using process_options.m usually results in readable and maintainable option definitions.

    Example code:

    function y = func(x, y, varargin)
    
        [u, v] = process_options(varargin,
                                 'u', 0,
                                 'v', 1);
    
    0 讨论(0)
提交回复
热议问题