Untapped Potential for Output-arguments Block
goc3
on 19 Jun 2025 at 13:49
Latest activity Reply by Michelle Hirsch
on 27 Jun 2025 at 10:59
Untapped Potential for Output-arguments Block
MATLAB has a very powerful feature in its arguments blocks. For example, the following code for a function (or method):
- clearly outlines all the possible inputs
- provides default values for each input
- will produce auto-complete suggestions while typing in the Editor (and Command Window in newer versions)
- checks each input against validation functions to enforce size, shape (e.g., column vs. row vector), type, and other options (e.g., being a member of a set)
function [out] = sample_fcn(in)
arguments(Input)
in.x (:, 1) = []
in.model_type (1, 1) string {mustBeMember(in.model_type, ...
["2-factor", "3-factor", "4-factor"])} = "2-factor"
in.number_of_terms (1, 1) {mustBeMember(in.number_of_terms, 1:5)} = 1
in.normalize_fit (1, 1) logical = false
end
% function logic ...
end
If you do not already use the arguments block for function (or method) inputs, I strongly suggest that you try it out.
The point of this post, though, is to suggest improvements for the output-arguments block, as it is not nearly as powerful as its input-arguments counterpart. I have included two function examples: the first can work in MATLAB while the second does not, as it includes suggestions for improvements. Commentary specific to each function is provided completely before the code. While this does necessitate navigating back and forth between functions and text, this provides for an easy comparison between the two functions which is my main goal.
Current Implementation
The input-arguments block for sample_fcn begins the function and has already been discussed. A simple output-arguments block is also included. I like to use a single output so that additional fields may be added at a later point. Using this approach simplifies future development, as the function signature, wherever it may be used, does not need to be changed. I can simply add another output field within the function and refer to that additional field wherever the function output is used.
Before beginning any logic, sample_fcn first assigns default values to four fields of out. This is a simple and concise way to ensure that the function will not error when returning early.
The function then performs two checks. The first is for an empty input (x) vector. If that is the case, nothing needs to be done, as the function simply returns early with the default output values that happen to apply to the inability to fit any data.
The second check is for edge cases for which input combinations do not work. In this case, the status is updated, but default values for all other output fields (which are already assigned) still apply, so no additional code is needed.
Then, the function performs the fit based on the specified model_type. Note that an otherwise case is not needed here, since the argument validation for model_type would not allow any other value.
At this point, the total_error is calculated and a check is then made to determine if it is valid. If not, the function again returns early with another specific status value.
Finally, the R^2 value is calculated and a fourth check is performed. If this one fails, another status value is assigned with an early return.
If the function has passed all the checks, then a set of assertions ensure that each of the output fields are valid. In this case, there are eight specific checks, two for each field.
If all of the assertions also pass, then the final (successful) status is assigned and the function returns normally.
function [out] = sample_fcn(in)
arguments(Input)
in.x (:, 1) = []
in.model_type (1, 1) string {mustBeMember(in.model_type, ...
["2-factor", "3-factor", "4-factor"])} = "2-factor"
in.number_of_terms (1, 1) {mustBeMember(in.number_of_terms, 1:5)} = 1
in.normalize_fit (1, 1) logical = false
end
arguments(Output)
out struct
end
%%
out.fit = [];
out.total_error = [];
out.R_squared = NaN;
out.status = "Fit not possible for supplied inputs.";
%%
if isempty(in.x)
return
end
%%
if ((in.model_type == "2-factor") && (in.number_of_terms == 5)) || ... % other possible logic
out.status = "Specified combination of model_type and number_of_terms is not supported.";
return
end
%%
switch in.model_type
case "2-factor"
out.fit = % code for 2-factor fit
case "3-factor"
out.fit = % code for 3-factor fit
case "4-factor"
out.fit = % code for 4-factor fit
end
%%
out.total_error = % calculation of error
if ~isfinite(out.total_error)
out.status = "The total_error could not be calculated.";
return
end
%%
out.R_squared = % calculation of R^2
if out.R_squared > 1
out.status = "The R^2 value is out of bounds.";
return
end
%%
assert(iscolumn(out.fit), "The fit vector is not a column vector.");
assert(size(out.fit) == size(in.x), "The fit vector is not the same size as the input x vector.");
assert(isscalar(out.total_error), "The total_error is not a scalar.");
assert(isfinite(out.total_error), "The total_error is not finite.");
assert(isscalar(out.R_squared), "The R^2 value is not a scalar.");
assert(isfinite(out.R_squared), "The R^2 value is not finite.");
assert(isscalar(out.status), "The status is not a scalar.");
assert(isstring(out.status), "The status is not a string.");
%%
out.status = "The fit was successful.";
end
Potential Implementation
The second function, sample_fcn_output_arguments, provides essentially the same functionality in about half the lines of code. It is also much clearer with respect to the output. As a reminder, this function structure does not currently work in MATLAB, but hopefully it will in the not-too-distant future.
This function uses the same input-arguments block, which is then followed by a comparable output-arguments block. The first unsupported feature here is the use of name-value pairs for outputs. I would much prefer to make these assignments here rather than immediately after the block as in the sample_fcn above, which necessitates four more lines of code.
The mustBeSameSize validation function that I use for fit does not exist, but I really think it should; I would use it a lot. In this case, it provides a very succinct way of ensuring that the function logic did not alter the size of the fit vector from what is expected.
The mustBeFinite validation function for out.total_error does not work here simply because of the limitation on name-value pairs; it does work for regular outputs.
Finally, the assignment of default values to output arguments is not supported.
The next three sections of sample_fcn_output_arguments match those of sample_fcn: check if x is empty, check input combinations, and perform fit logic. Following that, though, the functions diverge heavily, as you might expect. The two checks for total_error and R^2 are not necessary, as those are covered by the output-arguments block. While there is a slight difference, in that the specific status values I assigned in sample_fcn are not possible, I would much prefer to localize all these checks in the arguments block, as is already done for input arguments.
Furthermore, the entire section of eight assertions in sample_fcn is removed, as, again, that would be covered by the output-arguments block.
This function ends with the same status assignment. Again, this is not exactly the same as in sample_fcn, since any failed assertion would prevent that assignment. However, that would also halt execution, so it is a moot point.
function [out] = sample_fcn_output_arguments(in)
arguments(Input)
in.x (:, 1) = []
in.model_type (1, 1) string {mustBeMember(in.model_type, ...
["2-factor", "3-factor", "4-factor"])} = "2-factor"
in.number_of_terms (1, 1) {mustBeMember(in.number_of_terms, 1:5)} = 1
in.normalize_fit (1, 1) logical = false
end
arguments(Output)
out.fit (:, 1) {mustBeSameSize(out.fit, in.x)} = []
out.total_error (1, 1) {mustBeFinite(out.total_error)} = []
out.R_squared (1, 1) {mustBeLessThanOrEqual(out.R_squared, 1)} = NaN
out.status (1, 1) string = "Fit not possible for supplied inputs."
end
%%
if isempty(in.x)
return
end
%%
if ((in.model_type == "2-factor") && (in.number_of_terms == 5)) || ... % other possible logic
out.status = "Specified combination of model_type and number_of_terms is not supported.";
return
end
%%
switch in.model_type
case "2-factor"
out.fit = % code for 2-factor fit
case "3-factor"
out.fit = % code for 3-factor fit
case "4-factor"
out.fit = % code for 4-factor fit
end
%%
out.status = "The fit was successful.";
end
Final Thoughts
There is a significant amount of unrealized potential for the output-arguments block. Hopefully what I have provided is helpful for continued developments in this area.
What are your thoughts? How would you improve arguments blocks for outputs (or inputs)? If you do not already use them, I hope that you start to now.
17 Comments
Time DescendingI like the thought about output arguments.
I would also very much like to see MathWorks provide introspection functions for the arguments block (for vanilla functions, not classes). Allow me to query the valid input and output arguments. I have a kludgy function of my own that uses a regex that only supports a subset of possible arguments. Here is the alternative that someone did on stackoverflow (which is maybe less of a kludge than a regex, but does not perform well):
Introspection in MATLAB is kind of half baked. I would like to see that change.
> the mustBeSameSize validation function that I use for fit does not exist, but I really think it should
Yes, please! There some very obvious helper functions missing and, while you can write your own, it's often a bit balky and it's less effective as documentation.
It's not clear to me how to retrofit arguments blocks onto code that uses empty arrays (i.e., [ ]) to skip required arguments. For example, max(data, [], dim) to find the maximum along a particular dimension. Perhaps it would be nice to permit multiple argument blocks and indicate which one matched, like
arguments(calltag='WithinArray')
A
~ mustBeEmpty
dim
end
arguments(calltag='AcrossArrays')
A
B mustBeSameSize(B, A)
end
if calltag == 'OneArray'
% Do stuff
else
This would get you something like (limited) multiple dispatch, which I do like in Julia. (Arguably, you should just avoid writing code like this--but there's a lot of code using this pattern even within Matlab itself).
This doesn't deliver what you're describing but it may be a workaround that meets you half way. This function uses a separate validation function that could be included at the bottom of the m-file (although it can't be a nested function).
Note that some of the initial output values differ in class and size due to the arguments block validation.
One issue, though, is that error stacks will show that the validateOutputs function failed rather than the sample_fcn. I suppose the validateOutputs function could be wrapped in a try/catch if that's a concern.
A = sample_fcn()
function out = sample_fcn()
out.fit = uint8(1:5); % Row vector of int8 values
out.total_error = 0;
out.status = 'OK'; % Character vector
% Validate outputs
outPairs = namedargs2cell(out);
out = validateOutput(outPairs{:});
end
function S = validateOutput(S)
arguments
S.fit (:,1) double % forces column vector; converts to double
S.total_error (1,1) {mustBeFinite}
S.status string {mustBeTextScalar} % Converts to string
end
end
Thanks for this, Grant. A lot of interesting ideas. It's funny - when we first set down to add the Outputs block it seemed like the requirements were obvious, but as we actually worked on it we realized they weren't. My biggest hope for it has been that the language could use type definitions as cues to help when writing code. The ability to statically know what will come out of a function seems like it would support better code completion workflows and perhaps even help statically identify potential bugs where users are passing around arguments of incompatible types.
I don't see as direct of a mapping from input block features to output block features as you do, but I think there's potentially some promise. There isn't really a semantic meaning to default values of output variables as there is with inputs, though it is relevant to set default values to members of variables - typically object properties, or struct fields as you've done here. Object properties already have a natural syntax for declaring defaults; your example points out that there's no equivalent for structs. Regardless, I can see see the conveneinece for some coding patterns of ensuring you've set values before the function ends. It's not how I code, but I see the value for your coding style that relies heavily on ifs and switches. I don't know if your style is common enough to motivate a language feature, but it could be.
I'm less sold on your idea of supporting name-value pairs for outputs, since that really doesn't have a mapping into anything in MATLAB. It's quite specific to your design pattern (which I see others adopt) of packing all outputs into a single struct.
Sign in to participate