Functional programming: looking to create functions that map f(p1,p2,p3,...,pN,x) to g([p1 p2 p3 ... pN],x) and the reverse

4 views (last 30 days)
I'd like to have two functions, one that takes a function of this form:
f=@(p1,p2,...,pN,x);
and returns a function handle that takes a vector of N arguments and unpacks it:
g=fpnx_to_px(f,N);
such that
g=@(p,x)(f(p(1),p(2),...,p(N),x));
and an "inverse" function:
h=fpx_to_pnx(g,N);
such that
h=@(p1,p2,...,pN,x)(g([p1 p2 ... pN],x));
I have a couple functions that partially work:
% generates wrapper that converts calling convention for f from f(p1,...pn,x)
% to g(px,x), where p is a vector with n parameters
function g = fpnx_to_px(f,n)
pstr = '';
for idx=1:n
pstr = [pstr 'p' num2str(idx) ' '];
end
% eliminate trailing space
pstr = pstr(1:end-1);
evalstr = ['@(' strrep(pstr,' ',',') ',x)(f([' pstr '],x))'];
g = evalin('caller',evalstr);
end
% generates wrapper that converts calling convention for f from f(p,x),
% where p is a vector with n parameters, to g(p1,...,pn,x)
function g = fpx_to_pnx(f,n)
pstr = '';
for idx=1:n
pstr = [pstr 'p(' num2str(idx) '),'];
end
% eliminate trailing comma
pstr = pstr(1:end-1);
evalstr = ['@(p,x)(f(' pstr ',x))'];
g = evalin('caller',evalstr);
end
so if I do the following
f=@(p1,p2,p3,x)(p1+p2+p3*x);
g=@(p,x)(p(1)+p(2)+p(3)*x);
f(1,2,3,1:4) -> [6 9 12 15]
g([1 2 3],1:4) -> [6 9 12 15]
gg=fpx_to_pnx(f,3);
gg([1 2 3],1:4) -> [6 9 12 15]
ff=fpnx_to_px(g,3);
ff(1,2,3,1:4)
gives an error:
Matrix dimensions must agree.
Error in @(p1,p2,p3,x)(p1+p2+p3*x)
Error in fpnx_to_px>@(p1,p2,p3,x)(f([p1,p2,p3],x))
which seems to be coming from the definition of f within fpnx_to_px coming from the definition in the workspace. I think what I need is a way to get evalin to expand the argument f within the function in to the actual function handle but am clearly missing something. Perhaps there's a better way to do this that doesn't involve evalin at all?
  2 Comments
Matthias Schabel
Matthias Schabel on 30 Dec 2022
Even better would be to have the functions figure out the number of arguments so you don't have to pass N...
Stephen23
Stephen23 on 2 Jan 2023
Edited: Stephen23 on 2 Jan 2023
Note that the examples are inconsistent with the explanation.
For example the function fpnx_to_px is described as "from f(p1,...pn,x) to g(px,x)". In the examples the function f does indeed have multiple p-values, however it is modified with fpx_to_pnx:
f=@(p1,p2,p3,x)(p1+p2+p3*x);
..
gg=fpx_to_pnx(f,3);
gg([1 2 3],1:4)

Sign in to comment.

Accepted Answer

Stephen23
Stephen23 on 2 Jan 2023
Edited: Stephen23 on 2 Jan 2023
Here are two wrapper functions:
fpx_to_fpnx = @(fnh) @(varargin) fnh([varargin{1:end-1}],varargin{end});
fpnx_to_fpx = @(fnh) @(p,x) fnh(cell2struct(num2cell(p),'p').p,x);
Tested using your examples (modified by removing the input N):
f=@(p1,p2,p3,x)(p1+p2+p3*x);
g=@(p,x)(p(1)+p(2)+p(3)*x);
f(1,2,3,1:4)
ans = 1×4
6 9 12 15
g([1,2,3],1:4)
ans = 1×4
6 9 12 15
gg = fpnx_to_fpx(f);
gg([1,2,3],1:4)
ans = 1×4
6 9 12 15
ff = fpx_to_fpnx(g);
ff(1,2,3,1:4)
ans = 1×4
6 9 12 15
I recommend placing x as the first input argument, which gives simpler, cleaner, and clearer code:
fxp_to_fxpn = @(fnh) @(x,varargin) fnh(x,[varargin{:}]);
fxpn_to_fxp = @(fnh) @(x,p) fnh(x,cell2struct(num2cell(p),'p').p);
However code like this is something to play with and should be avoided in anything serious: it would get in the way of the JIT engine doing its work, and severely limits the sizes and types of data that you can call those functions with. Prefer working with arrays as much as possible.

More Answers (2)

George Abrahams
George Abrahams on 30 Dec 2022
You're so close! You just need to understand Comma-Separated Lists and varargin.
g = @(f,x,p) f( x, p{:} );
h = @(f,x,varargin) f( x, cell2mat(varargin) );
g( @cat, 2, ["super" "cali" "fragil" "istic"] )
% ans = 'supercalifragilistic'
h( @max, 5, 2, 4, 6, 8 )
% ans = [5, 5, 6, 8]
You're example is a little confused but this should point you in the right direction.
  2 Comments
Matthias Schabel
Matthias Schabel on 31 Dec 2022
Hi George,
Thanks for your answer. I think this is really close to what I'm looking for, and works great, but I want to get a function handle that wraps the call to the function argument completely so that I don't have to pass the handle to the original function around everywhere. That is, I want something like:
newfun = @(fun)(@(p,x)fun(p{:},x));
so that, if I define
g = @(p,x)(p(1)+x.*(p(2)+x.*p(3)));
and
gg = newfun(g);
then
gg(1,2,3,1:4)
is equivalent to
g([1 2 3],1:4)
George Abrahams
George Abrahams on 31 Dec 2022
Edited: George Abrahams on 31 Dec 2022
Hi @Matthias Schabel, I strongly encourage you to define a hard-coded wrapper function for each function, like this:
oneargcat = @(dim,An) cat( dim, An{:} );
oneargcat( 2, ["super" "cali" "fragil" "istic"] )
ans = 'supercalifragilistic'
nargmax = @(varargin) max(cell2mat(varargin));
nargmax( 3, 1, 4, 1, 6 )
ans = 6
It's clearer and allows flexibility of the arguments. To make that clear, consider the following, with args2vector and vector2args inspired by @Stephen23's answer below.
vector2args = @(f) @(vec) f(cell2struct(num2cell(vec),'a').a);
oneargcat = vector2args(@cat);
oneargcat( [2, 1 2 3 4 5] )
ans = 1×5
1 2 3 4 5
The above is effectively cat( 2, 1:5 ) and works great. However, as all input arguments must be concatenated into a vector, all functions wrapped by vector2args must be of the form f( a1, a2, a3, ..., an ), where a1,a2,a3,...,an are all scalar and the same class. Hence, neither of these are possible:
  • cat( 2, "super", "cali", "fragil", "istic" )
  • cat( 1, zeros(2), ones(2) )
args2vector = @(f) @(varargin) f(varargin{1},[varargin{1:end}]);
nargmax = args2vector(@max);
nargmax( 3, 1, 2, 3, 4, 5 )
ans = 1×6
3 3 3 3 4 5
The above is effectively max( 3, 1:5 ) and, again, works great. However, it is hardcoded that for all functions wrapped by args2vector must be of the form f( a, [b1,b2,...,bn] ). Hence, none of the following are possible:
  • max( 1:5, 3 )
  • max( [1 2; 3 4] )
  • max( rand(3), [], 1 )
However, even hardcoding each function as I've suggested has its limits. One is that you can't pass matrices, without also passing/hardcoding the shape, as varargin is always a 1-by-N vector. Another is that you can only have one varargin argument. That is, you would not be able to convert N arguments to 2 vector arguments without passing/hardcoding the length of those vectors, e.g. max( 1:5, 5:-1:1 ).

Sign in to comment.


John D'Errico
John D'Errico on 30 Dec 2022
Edited: John D'Errico on 30 Dec 2022
I think you are only part way along in your quest, but that your quest will never be happily fulfilled.
You are asking to have a tool that will automatically generate the inverse of a potentially arbitrarily nonlinear function of multiple variables, and to do so as a function.
Remember that the inverse of a nonlinear function is generally not a simple single valued function. For example, what inverse function would you return for the trivial function
y = @(x) x.^2;
fplot(y,[-1,1])
xlabel X
ylabel Y
grid on
where x varies on the interval [-1,1]?
The inverse is not uniquely defined. This is in fact quite the expected result, where for even trivially simple functions, there is no inverse.
At best, you will now be faced with using a nonlinear rootfinder for any set of inputs for the inverse, and will be HOPING it always converges. Even if it does converge, the solution it converges to may not be in the domain of interest. Even for two points near each other in the range space of your original function, the solver may not converge to solutions that are close to each other.
Hey, good luck. But I think you are investing a lot of time in thinking how to formulate the problem in MATLAB, before you even think about whether this is a well-defined problem to solve at all.
  3 Comments
John D'Errico
John D'Errico on 2 Jan 2023
So all you want is a vector splitter, and a combiner, each of which can act as a wrapper of sorts?
Assume we have a function that takes split arguments. Rosenbrock, here:
Mysplitfun = @(x1,x2) (1 - x1).^2 + 100*(x2 - x1.^2).^2;
Now we want to optimize it. I'll use fminsearch.
x0 = [3 3];
[X,Fval] = fminsearch(@(V) splitter(V,Mysplitfun),x0)
X = 1×2
1.0000 1.0000
Fval = 1.0423e-09
function obj = splitter(V,fun)
% splits a vector automatically into multiple arguments, then used by fun
Vsplit = mat2cell(V,1,ones(size(V)));
obj = fun(Vsplit{:});
end
I'm not at all sure what the use case that you see for the reverse operation is. But a combiner code might just use cell2mat.
function V = combiner(varargin)
% combines a list of scalar arguments into a single vector
V = cell2mat(varargin);
end

Sign in to comment.

Products


Release

R2022a

Community Treasure Hunt

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

Start Hunting!