Functional programming construct to expand cell array into arguments for other functions without using an intermediate variable in the (user) code?

I'm looking for a functional programming construct. The construct should let me use a cell array, e.g. returned from a function, as separate arguments to another function -- without (the user) having to create an intermediate variable.
Example: Let's say I have two objects (tables), T1 and T2, for which I'd like to compare different properites (e.g. size, variable names). I could do it as follows (doing the example for just one property):
property_1 = @(T) T.Properties.VariableNames;
C = cellfun(property_1, { T1, T2 }, 'UniformOutput', 0); % (1)
assert(isequal(C{:})); % (2)
However, I'd like a way to achieve (1) and (2) without assigning to the intermediate variable, 'C'. Unfortunately the following code doesn't work:
assert(isequal(cellfun(varnames, { T1, T2 }, 'uni', 0){:}));
Is it possible to write a helper function 'foo' that would allow this code:
assert(isequal(foo(result)))
Or must there instead be another type of helper function, 'bar', that's used along the following lines:
assert(bar(@isequal, result))
function [varargout] = bar(fcn, C)
[varargout{1:nargout}] = fcn(C{:});
end
If I can only do the latter, any suggestions for the name of 'bar'?

2 comentarios

is there a reason not mentioned here that you must use this cell array route, rather than a strategy such as
function assertPairwisePropertyEquality(A,B,PropName)
PropA = A.Properties.(PropName);
PropB = B.Properties.(PropName);
assert(isequal(PropA,PropB))
end
Or maybe
propFcn = @(obj)(obj.Properties.VariableNames);
% propFcn = @(obj)(height(obj));
function assertPairwisePropertyEquality(A,B,fcn)
PropA = fcn(A);
PropB = fcn(B);
assert(isequal(PropA,PropB))
end
Yes, there are reasons why I'd like an alternative to the strategy above. That strategy is fine when implementing the tests in a .m-file. I would use the second alternative though and generalise it into something like the following to support multiple arguments and choosing between e.g. 'isequal' and 'isequaln':
% Example:
% assert_comparable_properties(@size, @isequal, T1, T2)
% Caveat: Untested code!
function assert_comparable_properties(property, comparator, varargin)
if isequal(comparator, []), comparator = @isequal; end
D = cellfun(property, varargin, 'uni', 0);
assert(comparator(D{:}), 'Comparison of objects failed')
% If 'comparator' only takes two arguments, use this for loop:
% for ii = 2:numel(D)
% assert(comparator(D{ii-1}, D{ii}), ...
% 'Comparison of objects: %d and %d failed', ii-1, ii)
% end
end
However, while experimenting and running tests on the command line, it's convenient if I can use anonymous functions. (If I remember correctly, you can't use 'function() ... end' on the MATLAB command line).
Then I just generally like to learn alternative ways of doing things - you never know when it's useful.
Finally, I know I have on several other occasions encountered situations where I'd like to expand e.g. output of cellfun(). I think I e.g. wanted to do something like:
C = cellfun(...)
y = vertcat(C{:})
Note: I don't remember the details, but I think I was generating combinations of parameter sets to be used for running suites of Simulink simulations. I.e. I had a bunch of parameters for which a small subset of the parameter could only take on a few discrete values and I wanted to generate all combinations of all such parameters. I then ended up writing a framework that turned out ok, except I missed being able to do the above.

Iniciar sesión para comentar.

 Respuesta aceptada

The closest is to use the new syntax which allows dot indexing directly into function outputs:
For example:
fprintf('%d\n',testfun().X)
123 456 789
function S = testfun()
S(1).X = 123;
S(2).X = 456;
S(3).X = 789;
end
Otherwise the reason why your approach will not work is already explained in detail in this comment (and thread):

5 comentarios

forgive me if I'm completely misunderstanding...but I think the question of "generating a comma separated list inline" is slightly different from the question of why a function that returns a single variable cannot be directly indexed (which I think is ultimately the core of this question)? I have been curious about this for a while, though i'm certainly not making any judgements and fully admit ignorance on all the considerations and design of languages...
c = myfn()
out = 1×3 cell array
{[2]} {'hi'} {[6]}
c = 1×3 cell array
{[2]} {'hi'} {[6]}
c{3}
ans = 6
but why not
myfn(){3}
Invalid array indexing.

Error in connector.internal.fevalMatlab

Error in connector.internal.fevalJSON
function out = myfn()
out = {2,'hi',6};
end
And if the "dot" notation ultimately is reliant on subsref (or is it no longer with the new syntax?), what distinguishes the {} from . when it comes to directly indexing the result of a function from its function call?
Again, sorry if I'm not making any sense...
"why a function that returns a single variable cannot be directly indexed (which I think is ultimately the core of this question)?"
The OP asked: "Is it possible to write a helper function 'foo' that would allow this code: assert(isequal(foo(result)))"
This is what I answered in the link, and corresponds(?) to your comment "generating a comma separated list inline" directly as a function output. Note there is no indexing into the output in what the OP actually asked for.
Direct indexing into the function output is shown in one of the OP's examples (using curly braces, does not work), and in the code I showed (using dot indexing, works).
Thus my answer addresses both of those topics.
"And if the "dot" notation ultimately is reliant on subsref..."
I do not know these implementation details for comma-separated lists. As far as I can tell, the comma-separated list syntax is interpreted slightly differently than normal indexing (i.e. SUBSREF function output).
I do not know why direct indexing of the function output using curly braces is not allowed.
Thanks for your answer, I greatly appreciated learning about the new dot-thing.
To help other users, could you please somehow include your comment below in the answer:
The OP asked: "Is it possible to write a helper function 'foo' that would allow this code: assert(isequal(foo(result)))"
That's really the core of my question (sadly with a negative answer).
Then perhaps mention the workaround a'la: my 'use_expand()' approach below.
Finally, if you know a good/common name for 'use_expand', that'd answer the last part of my question.
See the answer below from @J. Alex Lee and also my comment where I create a helper function 'cellfun_s'.
Inspired by that answer, it allows doing e.g. the following - what to call things is the unclear part, hence it's 'foo' for now:
% Define helper function
foo = @(C) cell2struct(C, 'expand_as_list');
% Place some dummy results in a cell array
result_as_cell_array = {rand(1) < 0.5, rand(1) < 0.5}
result_as_cell_array = 1×2 cell array
{[1]} {[0]}
% Use 'foo(<x>).expand_as_list' to pass the expanded cell array as arguments to 'isequal'
isequal(foo(result_as_cell_array).expand_as_list)
ans = logical
0
Note: Does not work in R2018b, but does work in R2020b.

Iniciar sesión para comentar.

Más respuestas (2)

I am still not sure why you can't just have your generalized comparator function saved as a function somewhere on your path so you can reference it often...but anyway, "apply_expand" is still an intermediary, though you do accomplish it inline...
With @Stephen Cobeldick's insight, I thnk you could as well do (it would be just as cumbersome but you can at least remove one anonymous function definition)
isequal(cell2struct(cellfun(property_1, { T1, T2 }, 'UniformOutput', 0),"myprop").myprop)
To me, it all points to the more upstream question of why you can dot-index, but not brace-index into function calls...it doesn't seem at all silly to expect that it should work, and it seems to me that many questions along the lines of the actual question here (as well as the linked one) might ultimately derive from this inability.

2 comentarios

Personally I think it's a annoying flaw in MATLAB that you can't brace-index into the temporary result. However, I suspect it's non-trivial for Mathworks to fix this, otherwise I think they'd done it already.
I'll have to look into your suggestion of going via a struct, that might be useful but I can't wrap my head around it yet.
Note: In Octave it works just fine to brace-index (I just checked it in https://octave-online.net/ ):
octave:1> foo = @(x) { x+1, x+2}; foo(5), foo(5){2}
ans =
{
[1,1] = 6
[1,2] = 7
}
ans = 7
Seems to work well.
For readability I created and used a helper 'cellfun_s()' as follows:
% Helper function like 'cellfun', but with the output placed in the field
% '.as_list' of a struct array.
cellfun_s = @(varargin) cell2struct(cellfun(varargin{:}, 'UniformOutput', 0), 'as_list');
% Create some objects to be compared
objs = { table(rand(5)), table(rand(5)), table(rand(5)) };
% Make a smörgåsbord of ways to get aspects/properties from the objects (to be compared)
getters = {@size, @(T) T.Properties.VariableNames};
% Compare the objects with regards to the first aspect/property
comparison_result_1 = isequal(cellfun_s(getters{1}, objs).as_list)
comparison_result_1 = logical
1
The approach also scales for multiple comparisons:
% Compare all (2) aspects for all (3) the objects
comparison_results = cellfun(@(pg) isequal(cellfun_s(pg, objs).as_list), getters)
comparison_results = 1×2 logical array
1 1
assert(all(comparison_results))

Iniciar sesión para comentar.

Here is an answer that follows the last approach in my question, using the name 'apply_expand' for 'bar':
property_1 = @(T) T.Properties.VariableNames;
apply_expand = @(fcn, C) fcn(C{:});
assert(apply_expand(@isequal, cellfun(property_1, { T1, T2 }, 'UniformOutput', 0)));
Below is an exanded example, that compares more than two objects and works on the command line:
The key part is the 'apply_expand' construct that takes a comparison function like 'isequal' and a cell array argument, and applies 'isequal' to the exanded cell array.
The example uses two versions of 'map', depending on if you want to supply multiple arguments via a cell array or as separate arguments. This then leads to having to use 'arrayfun' or 'cellfun' respectively.
%% Prepare three objects to be compared
objs = { table(rand(5)), table(rand(5)), table(rand(5)) };
% Selectors for the aspects to compare:
get_aspects_to_compare = { @size, @(T) T.Properties.VariableNames };
%% Helper functions
apply_expand = @(fcn, C) fcn(C{:}); % Example: apply_expand(@isequal, {1, 1, 1})
cellfun_c = @(varargin) cellfun (varargin{:}, 'UniformOutput', false);
arrayfun_c = @(varargin) arrayfun(varargin{:}, 'UniformOutput', false);
% Alt 1, example: map_1(get_aspects_to_compare, objs(1))
map_1 = @(fcns, values) cellfun_c(@(f) f(values{:}), fcns);
assert(apply_expand(@isequal, arrayfun_c(@(o) map_1(get_aspects_to_compare, o), objs)))
% Alt 2, example: map_1(get_aspects_to_compare, objs{1})
map_2 = @(fcns, varargin) cellfun_c(@(f) f(varargin{:}), fcns);
assert(apply_expand(@isequal, cellfun_c( @(o) map_2(get_aspects_to_compare, o), objs)))

1 comentario

In practice I'd use something like the above, but I'll accept Stephens answer in a while as:
a) It answered that I cannot do: assert(isequal(foo(result)))
b) I very much appreciated learning about being able to do: foo().something.

Iniciar sesión para comentar.

Categorías

Más información sobre Data Type Identification en Centro de ayuda y File Exchange.

Etiquetas

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!

Translated by