I’ve seen a lot of ugly implementations from people trying to deal with variable number of input and output arguments. The most horrendous one I’ve seen so far came from MIT’s Physionet’s WFDB MATLAB Toolbox. Here’s a snippet showing how wfdbdesc.m
handles variable input and output arguments:
function varargout=wfdbdesc(varargin) % [siginfo,Fs,sigClass]=wfdbdesc(recordName) ... %Set default pararamter values inputs={'recordName'}; outputs={'siginfo','Fs','sigClass'}; for n=1:nargin if(~isempty(varargin{n})) eval([inputs{n} '=varargin{n};']) end end ... if(nargout>2) %Get signal class sigClass=getSignalClass(siginfo,config); end for n=1:nargout eval(['varargout{n}=' outputs{n} ';']) end
The code itself reeks a very ‘smart’ beginner who didn’t RTFM. The code is so smart (shows some serious thoughts):
- Knows to use
nargout
to controlvarargout
to avoid the side effects when no output is requested - [Cargo cult practice]: (unnecessarily) track your variable names
- [Cargo cult practice]: using
varargin
so it can be symmetric tovarargout
(also handled similarly).varargout
might have a benefit mentioned above, but there is absolutely no benefit to usevarargin
over direct variable names when you are not forwarding or useinputParser()
. - [Cargo cult practice]: tries to be efficient to skip processing empty inputs. Judicially non-symmetric this time (not done to output variables)!
but yet so dumb (hell of unwise, absolutely no good reason for almost every ‘thoughtful’ act put in) at the same time. Definitely MIT: Make it Tough!
This code pattern is so wrong in many levels:
- Unnecessarily obscuring the names by using
varargin
/varargout
- Managing a list of variable names manually.
- Loop through each item of
varargin
andvarargout
cells unnecessarily - Use
eval()
just to do simple cell assignments! Makes me cringe!
Actually, eval()
is not even needed to achieve all the remaining evils above. Could have used S_in = cell2struct(varargin)
and varargout=struct2cell(S_out)
instead if one really wants to control the list of variable names manually!
The hurtful sins above came from not knowing a few common cell packing/unpacking idioms when dealing with varargin
and varargout
, which are cells by definition. Here are the few common use cases:
- Passing variable arguments to another function (called perfect forwarding in C++): remember
C{:}
unpacks to comma separated lists!function caller(varargin) callee(varargin{:});
- Limiting the number of outputs to what is actually requested: remember
[C{:}]
on left hand side (assignment) means the outputs are distributed as components ofC
that would have been unpacked as comma separated lists, i.e.[C{:}] = f();
means[C{1}, C{2}, C{3}, ...] = f();
function varargout = f() // This will output no arguments when not requested, // avoiding echoing in command prompt when the call is not terminated by a semicolon [varargout{1:nargout}] = eig(rand(3));
- You can directly modify varargin and varargout by cells without de-referencing them with braces!
function varargout = f(varargin) // This one is effectively deal() varargout = varargin(1:nargout); end function varargout = f(C) // This one unpacks each cell's content to each output arguments varargout = C(1:nargout); end
One good example combining all of the above is to achieve the no-output argument example in #2 yet neatly return the variables in the workspace directly by name.
function [a, b] = f() // Original way to code: will return a = 4 when "f()" is called without a semicolon a = 4; b = 2; end function varargout = f() // New way: will not return anything even when "f()" is called without a semicolon a = 4; b = 2; varargout = manage_return_arguments(nargout, a, b); end function C = manage_return_arguments(nargs, varargin) C = varargin(1:nargs); end
I could have skipped nargs
in manage_return_arguments()
and use evalin()
, but this will make the code nastily non-transparent. As a bonus, nargs
can be fed with min(nargout, 3)
instead of nargout
for extra flexibility.
With the technique above, wfdbdesc.m
can be simply rewritten as:
function varargout = wfdbdesc(recordName) % varargout: siginfo, Fs, sigClass ... varargout = manage_return_arguments(nargout, siginfo, Fs, sigClass);
Unless you are forwarding variable arguments (with technique#1 mentioned above), input arguments can be (and should be) named explicitly. Using varargin
would not help you avoid padding the unused input arguments anyway, so there is absolutely no good reason to manage input variables with a flexible list. MATLAB already knows to skip unused arguments at the end as long as the code doesn’t need it. Use exist('someVariable', 'var')
instead.