I could scroll this for hours, but I don't have a good gestalt representation of the overall Matlab signature processing. But here are some tips.
First take the laissez faire approach to test input types. Trust the caller. If you really need strong type testing, you need a static language like Java. Try to ensure type safety wherever you are in Matlab, and you will end up with a significant portion of your LOC and runtime designed to test runtime and enforcement in userland, which trades at high speed and Matlab development speed. I learned this hard way.
For API signatures (functions intended to be called from other functions, not from the command line), consider using a single Args argument instead of varargin. It can then be passed between multiple arguments without having to convert it to and from a comma-separated list for varargin signatures. Structures, Jonas says, are very comfortable. There is also a good isomorphism between n-by-2 structures and cells {name, value; ...}, and you can set up a couple of functions to convert between them inside your functions depending on what it wants to use inside.
function example(args) %EXAMPLE % % Where args is a struct or {name,val;...} cell array
Regardless of whether you use an input parser or collapse your own name / shaft parser, like these other great examples, pack it into a separate standard function that you will call from the top of your functions that have a name / sign signature. Whether to accept a list of default values in the data structure that is convenient to write, and your calls to analyze arguments will look like function signature declarations, which simplifies reading and avoids copy and paste template templates.
Parsing calls will look here.
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 is a function to implement name / val parsing this way. You could release it and replace it with an input parser, your own type conventions, etc. I think the n-by-2 cell convention does for well-readable source code; Think about it. Structures are generally more convenient for processing in the receive code, but n-by-2 cells are more convenient for building using expressions and literals. (Structures require the continuation of ", ..." in each row and protection of cell values from expansion to non-scalar structures.)
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 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 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 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
So unfortunate that strings and datamines are not first-class types in Matlab.