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.
Sean
Sean on 26 Jun 2025 at 13:54
I 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.
Michelle Hirsch
Michelle Hirsch on 26 Jun 2025 at 14:05
Thanks, @Sean. Could you say more about what you'd like to do with introspection functions for arguments blocks? We've been discussing the need for this; the more we know about what people specifically want to do the more likely we can design something useful.
Sean
Sean on 26 Jun 2025 at 15:22
Well, my current need is to automatically populate the valid arguments of a predetermined list of functions in a GUI (a UITable). Someone else is writing those functions (who is not familiar with MATLAB objects), and the input arguments of those functions are dynamic--meaning that they will change as our understanding of the problem changes. I don't want to make big changes to the GUI when the input arguments need to change. Fo example, the number of arguments, type of an existing argument or a default value might change. I need the GUI to automatically reflect those changes.
This is not the first time I've wanted to use introspection in MATLAB, though.
Matt
Matt on 25 Jun 2025 at 19:00
> 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).
Michelle Hirsch
Michelle Hirsch on 26 Jun 2025 at 11:37
This is interesting - when we designed arguments blocks, we actively explored design alternatives with multiple arguments blocks. I had assumed from the beginning it would be necessary to express the richness of common MATLAB function interface design patterns. It still may be quite helpful, but I've been very impressed with how much everyone has been able to adopt arguments blocks as they are.
Your last comment hits the nail on the head:
(Arguably, you should just avoid writing code like this--but there's a lot of code using this pattern even within Matlab itself).
One of the most important early steps in our design process for arguments blocks was to figure out which function design patterns to support. @Steve Eddins analyzed hundreds of documented functions from MathWorks to get a good map of common patterns. We assessed each design pattern to decide if we still liked it - was it making interfaces that are easy to use and lead to code that isn't a mess? Since many of these patterns had been around for decades, we assessed if more modern capabilities in the language would allow for better alternatives. So, we put our hand on the scale a bit (without trying to tell people how to design!).
We intentionally decided that the empty pattern like max(data, [], dim) was not worth propagating further even though it has such history in MATLAB. A quick look at the syntax block for max shows that it's not working that well, if for most uses you just have to mysteriously insert an empty array. The design is too heavily influenced by finding the max between two arrays, instead of the max of elements within a single array:
What do you all think? Are there other MATLABy design patterns you think are really good that should be easier to achieve with arguments blocks? The top of my list is the optional leading argument used to specify a parent throughout graphics - e.g. plot(X) or plot(ax,X). A rational person could argue that the design would be clearer with a named argument plot(X,Parent=ax). That rational person can happily write that code today, but I've not ready to give up on the compact plot(ax,X) I've used for so many years.
goc3
goc3 on 26 Jun 2025 at 20:26
Having dabbled quite a bit on Cody, I have seen many examples of very compact MATLAB code doing amazing things.
But, the older I get, the more I prefer explicit and clear APIs. Given the recent additions and improvements in the arguments blocks, name=value instead of name-value pairs, and autocomplete aspects (including in the Command Window), I would vote for a move towards the plot(X,Parent=ax) style. As great as the MATLAB documentation is, clearer APIs would enable coding with fewer flow-halting trips to the documentation.
And, while on that topic, it would be awesome if the name-value arguments box that appears when typing in a function/class signature would also display the default values for each of the supported inputs.
Michelle Hirsch
Michelle Hirsch on 27 Jun 2025 at 10:59
Thanks, Grant. As I said, a very reasonable person could make a good argument for plot(X,Parent=ax) 😀!
You make a good point about minimizing trips to the documentation with clearer APIs. Our historic design patterns for MATLAB functions was to strongly favor writeability over readability. Make it super quick and easy to type with short names, positional arguments. Autocompletion tools (and now, generative AI) allow us to shift the balance towards writeability, since a computer does much of the typing for you. I do find it very appealing to see a nice list of well named options pop up when calling a function - it advertises the capabilities of the function and as you said often helps me avoid going elsewhere for more help.
Great suggestion about showing defaults - I'll pass that along.
Steven Lord
Steven Lord on 25 Jun 2025 at 20:24
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.
If there are validation functions that you would like to see added to MATLAB, please suggest them as feedback (either using the Feedback button in the Toolstrip as of release R2025a or using the Contact Us link at the bottom of most or all of the pages on the MathWorks website.) Please include a description of how you hope or plan to use it if it is added to MATLAB.
Perhaps it would be nice to permit multiple argument blocks and indicate which one matched, like
What would you expect MATLAB to do if the way you called the function matched two or more of the argument blocks? For example, you might want to have the dim input argument default to the first non-singleton dimension of the first input like max does. Would calling this function with two empty arrays of the same size match the WithinArray or AcrossArrays "call tag"? Would dim be defined (with the default value based on the dimensions of the first input) or not? Would B be defined?
arguments(calltag='WithinArray')
A
~ mustBeEmpty
dim = firstNonsingletonDimension(A)
end
arguments(calltag='AcrossArrays')
A
B mustBeSameSize(B, A)
end
if calltag == 'OneArray'
% Do stuff
else
Adam Danz
Adam Danz on 19 Jun 2025 at 19:45 (Edited on 19 Jun 2025 at 20:30)
Interesting fodder @goc3.
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()
A = struct with fields:
fit: [5×1 double] total_error: 0 status: "OK"
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
goc3
goc3 on 19 Jun 2025 at 23:53
Adam, thanks for providing an example. That is a clever way to feed the struct into the validation function.
Also, your example points out that I could have replaced two assertions in my example with one, mustBeTextScalar. If only the MATLAB Editor could make suggestions for replacements like that...
Adam Danz
Adam Danz on 20 Jun 2025 at 14:03
Another convenience that mustBeTextScalar offers is that it recognizes that a character vector is intended to be a "scalar" text.
s = 'this';
isscalar(s)
ans = logical
0
However,
out = forceScalarString(s)
out = "this"
function s = forceScalarString(s)
arguments
s string {mustBeTextScalar}
end
end
Michelle Hirsch
Michelle Hirsch on 19 Jun 2025 at 18:16
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.
goc3
goc3 on 19 Jun 2025 at 23:44
Michelle, thanks for the response. It is often helpful that MATLAB is dynamically typed. I agree that more places to enforce static typing, when desired, would be helpful.
You are right that setting defaults for object properties is supported, and I do use that. I probably don’t use classes as often as many MATLAB users, hence my use of the function format as the example. Besides, I really like that functions can clearly define at the beginning what goes in and what comes out.
Then again, perhaps my approach is trying to merge certain aspects of classes into functions. One of the big strengths of MATLAB is its ability to cross pollinate ideas from all three programming paradigms.
For context, I have developed this pattern style after having created a system that interfaces with two third-party systems and is constantly live. Given their quirks, I have had to learn to program defensively, as requests for data sometimes return malformed data or no data at all. When such problems occur, my functions must gracefully return (with empty or default values) lest the whole system be halted for each such hiccup.
Michelle Hirsch
Michelle Hirsch on 23 Jun 2025 at 11:54
Thanks for the additional context.
Wearing my MATLAB design hat, if I were working on interfaces like yours I would typically replace the struct with an object. While it's not always a slam dunk that an object is better than a struct output, we've found a bunch of benefits:
  • It's easier to build multiple functions that work with it, since it's defined in one place and it's an identifiable thing instead of a generic struct that has to have just the right fields with just the right type values.
  • A well named object can be much more communicative of what it is than just a struct
  • There are ways to evolve the design over time (add/remove/change properties) without breaking backwards compatibility.
  • You can take advantage of all of the nice object features if they are helpful, like typing properties, making properties read only, making properties with computed values, etc.
I think optimoptions vs optimset is a good example of some of these benefits, especially for an object or struct that needs to be used in lots of different places.
The implementation of an object with just some basic properties is low effort. For me, the biggest argument against it is that you have to create an additional file for the class even if it's only ever created by one function. The ability to create "local classes" (like local functions) would address this issue. This is something we've discussed for years but haven't yet scheduled.
goc3
goc3 on 23 Jun 2025 at 19:12
I use local functions a lot, so local classes sound interesting.
You make compelling arguments for classes over functions. In your estimation, what (if any) situations are best suited for a function versus a class?
Michelle Hirsch
Michelle Hirsch on 23 Jun 2025 at 21:27
My argument here is for classes over structs, not classes over functions, and only from the perspective of API design, not implementation code. My previous comment explains the motivation; from a design perspective I can't think of many use cases off of the top of my head where a struct is better. The one exception is when the end user really wants a struct, which can be quite helpful if the fields are all or largely defined at runtime, e.g. by reading a file, as opposed to at design time, by a developer deciding what information to return. Of course, once you think a struct might be good it's also worth considering if a table would be better. The choice between table and struct comes down to the nature of the data returned - if you've got effectively a bunch of rows of data with named columns, a table is much more elegant than a struct.
I don't have any general biases towards classes or functions. The advice I often start with is: "If you are creating a class, does the end user actually think they want it?" Sometimes it's yes, sometimes it's no, sometimes it depends. A simple example is reading from a file:
One user says "I want to read this data from a file". Their goal is to get to data as soon as possible, so if you can support them with a function that gives them their data, that's great. Give 'em readtable.
Another user says "I want to incrementally read data from a file." This user might not know it, but they want a handle to the file that they can hold onto, so MATLAB can handle the bookkeeping of what they've read in the file. Give 'em a datastore.
Then you get the user who just wants to read from a file, but they need a lot of control over how the data comes in. They can set lots and lots of options in readtable until they get it right, but maybe they'd be better served with an options object that they can tweak. Give 'em detectImportOptions (a function) and the associated *importOptions objects it creates (classes).
goc3
goc3 on 23 Jun 2025 at 22:36
Thanks for the clarification. The options you present make sense; my work just seems to often fall outside those bounds.
For example, I often deal with heterogeneous data recorded by various instruments that measure material responses. There is usually a part of the data that is regular and can be saved as a table; that is, as you point out, the natural format for such data. However, it is often coupled with a set of metadata, which may range from scalars to vectors of many data types, thereby requiring a struct or cell array; the struct is preferable so that fields can be named. (Incrementally adding fields to a struct is easier than building a table, one column at a time.)
After processing, the code needs to return a variety of data formats and sizes (e.g., table of raw and/or processed data, metadata, status/log information, etc.); therefore, a struct makes sense to house all those outputs, unless a class is developed for that purpose. However, a lot of this work involves pipeline-like processing for which the intermediate stages/outputs are not ever needed, hence the desire to not spend time developing classes for each of those stages. The single-input, single-output format I presented makes such workflows very natural and easy/quick to build.
Also, because of the disparate formats that are output by various test instruments, I am often beyond the level of using options for readtable, instead using fgetl and associated low-level functions to reduce I/O to be as quick as possible while still enabling fast post-processing of raw data for hundreds to thousands of files.