function f1 = funcompose(f,F)
%function f1 = funcompose(f,F)
%
% Compose two or more function_handle by inferring intent from argument names.
%
% f function_handle
% F function_handle (or cell array thereof)
% f1 function_handle
%
% The second argument F may be either a function_handle satisfying isfun(F) or a cell array of
% function handles satisfying isfun(F{k}) for all k. In the former case, f1 will return a
% function handle implementing "f compose F" in the sense of function composition (implied by
% identifying named output arguments of F with similarly named input arguments of f). The new function
% takes a set of input arguments f1('-argin') comprising the union of F('-argin') and setdiff(f('-argin'),F('-argout')
% That is, it takes the minimal set of named arguments from f and F necessary to compute both f and F.
%
% The list of output arguments for f1 is the union of output arguments of f and the output arguments
% for F, but not in the same order. First, the output arguments of f are listed. Next, the output arguments
% of F which were not passed as input arguments to f (prerequisites). Finally, the output arguments of F which were not
% used in such a manner (side effects). To suppress prerequisites and side effects in f1 use the syntax
% f1 = funcompose(f,{F}) which is a special case of the second usage:
%
% If F is a cell array of function_handle then f1 will effect all relevant compositions, moving through the
% list of functions in topological order. For this to work, F should not contain cyclic intentions.
fmuse('/fun_util','at.m','isfun.m');
if ischar(f) && strcmpi(f,'test'),
f1 = test_compose;
elseif isa(f,'function_handle') && isa(F,'function_handle') && isfun(f) && isfun(F),
f1 = composeTwo(f,F);
elseif isa(f,'function_handle') && iscell(F) && isfun(f),
f1 = composeMany(f,F);
else
error('funcompose anticipates function handles meeting "isfun" criteria. See at.m');
end
function f1 = composeMany(f,F)
%function f1 = composeMany(f,F)
%
% f function_handle ("fun")
% F cell array of function_handle ("fun")
n = length(F);
A = intentGraph(F);
seq = toposort(A');
if isempty(seq),
error('funcompose. Cyclic dependency detected.');
else
f1 = f;
originalArgout = f1('-argout');
for k=1:n,
fk = F{seq(k)};
if isempty(intersect(f1('-argin'),fk('-argout'))),
% ignore fk
else
f1 = composeTwo(f1,fk);
% Reduce f1's output list to the original (ignore side effects)
attrib = {'-name',f1('-name'),'-argin',f1('-argin'),'-argout',originalArgout,'-fun',f1('-fun'),...
'-handle',f1('-handle'),'-attrib',f1('-attrib')};
f1 = at(f1('-handle'),attrib);
end
end
end
function A = intentGraph(F)
% F nFunc x 1 cell array of function_handle
% A nFunc x nFunc
% A(j,k)=1 if F{j}'s output can be used as input to F{k}
nFun = length(F);
A = zeros(nFun);
for j=1:nFun,
fjOut = feval(F{j},'-argout');
for k=1:nFun,
if j~=k,
fkIn = feval(F{k},'-argin');
A(j,k) = ~isempty(intersect(fjOut,fkIn));
end
end
end
function f1 = composeTwo(f,g)
% function f1 = composeTwo(f,g)
%
% f, g and f1 are annotated function handles ("fun")
% f1 is a function handle effecting the composition of f and g
%% Organize global list of input and output arguments
fIn = f('-argin'); fOut = f('-argout'); gIn = g('-argin');gOut = g('-argout');
gInOrOut = union(gOut,gIn);
[ignoreMe1,I1] = setdiff(fIn,gInOrOut); fInExtra=fIn(sort(I1)); % maintain original argument order
[ignoreMe2,I2] = setdiff(gOut,fIn); gOutUnused=gOut(sort(I2)); % maintain original argument order
[ignoreMe3,I3] = intersect(gOut,fIn); gOutUsed=gOut(sort(I3)); % maintain original argument order
[U,gIn_,gOut_,fIn_,fOut_,fInExtra_,gOutUnused_,gOutUsed_] = private_union(gIn,gOut,fIn,fOut,fInExtra,gOutUnused,gOutUsed);
argin_ = [gIn_;fInExtra_]; argin = U(argin_);
argout_ = [fOut_;gOutUnused_;gOutUsed_]; argout = U(argout_);
gName = g('-name');
if length(gName)<30,
name = [f('-name'),'_compose_',gName];
else
name = [f('-name'),'_compose_',gName(1:30),'_etc'];
end
%% Catch temporary limitations in the implementation
if length(gOut_)>12
error('funcompose currently allows at most 12 output arguments for g');
elseif length(fOut_)>12,
error('funcompose currently allows at most 12 output arguments for f');
elseif length(argout_)>20,
error('funcompose currently allows at most 24 combined output arguments');
end
%% Create new function handle,
fh = f('-handle'); gh = g('-handle'); % Using f and g would double the function calling overhead
f1h = @(varargin) jfeval(fh,gh,U,gIn_,fIn_,gOut_,fOut_,fInExtra_,gOutUnused_,gOutUsed_,varargin{:});
%% Attributes for the new fun
attrib = {'-name',name,'-argin',argin,'-argout',argout,'-fun',f('-fun'),...
'-handle',f1h,'-attrib',{'-name','-argin','-argout','-fun','-attrib','-handle'}};
f1 = at(f1h,attrib);
function [y1,y2,y3,y4,y5,y6,y7,y8,y9,y10,y11,y12,y13,y14,y15,y16,y17,y18,y19,y20,y21,y22,y23,y24]= jfeval(fh,gh,U,gIn_,fIn_,gOut_,fOut_,fExtra_,gOutUnused_,gOutUsed_,varargin)
% We evaluate fh and gh "jointly", following argument name intentions and using output from gh as input to fh.
n = length(U); V = cell(n,1);
k = length(gIn_);
V(gIn_) = varargin(1:k);
V(gOut_) = cfeval(gh,length(gOut_),varargin{1:k});
V(fExtra_) = varargin(k+1:k+length(fExtra_));
V(fOut_) = cfeval(fh,length(fOut_),V{fIn_});
cards = [fOut_;gOutUnused_;gOutUsed_]; % List relevant output arguments
padding = (n+1)*ones(24-length(cards),1); V{n+1} = nan; % Pass nan to extraneous output arguments
[y1,y2,y3,y4,y5,y6,y7,y8,y9,y10,y11,y12,y13,y14,y15,y16,y17,y18,y19,y20,y21,y22,y23,y24] = deal(V{[cards;padding]});
function y = cfeval(h,nArgOut,varargin)
%Return cell array rather than multiple outputs. Helper for jfeval
% h function_handle
if nArgOut<=0,
y = {};
else
y = cell(nArgOut,1);
switch nArgOut,
case 1, y{1} = h(varargin{:});
case 2, [y{1},y{2}] = h(varargin{:});
case 3, [y{1},y{2},y{3}] = h(varargin{:});
case 4, [y{1},y{2},y{3},y{4}] = h(varargin{:});
case 5, [y{1},y{2},y{3},y{4},y{5}] = h(varargin{:});
case 6, [y{1},y{2},y{3},y{4},y{5},y{6}] = h(varargin{:});
case 7, [y{1},y{2},y{3},y{4},y{5},y{6},y{7}] = h(varargin{:});
case 8, [y{1},y{2},y{3},y{4},y{5},y{6},y{7},y{8}] = h(varargin{:});
case 9, [y{1},y{2},y{3},y{4},y{5},y{6},y{7},y{8},y{9}] = h(varargin{:});
case 10, [y{1},y{2},y{3},y{4},y{5},y{6},y{7},y{8},y{9},y{10}] = h(varargin{:});
case 11, [y{1},y{2},y{3},y{4},y{5},y{6},y{7},y{8},y{9},y{10},y{11}] = h(varargin{:});
case 12, [y{1},y{2},y{3},y{4},y{5},y{6},y{7},y{8},y{9},y{10},y{11},y{12}] = h(varargin{:});
otherwise
error('cfeval: Too many output arguments requested');
end
end
function comparison = test_compose
%
fmuse('/fun_util','at.m');
fmuse('/fun_util/examples','funcompose_test_f.m','funcompose_test_g.m','funcompose_test_d.m','funcompose_test_w.m');
test_f = at('funcompose_test_f');
test_g = at('funcompose_test_g');
%% Test combination of two functions
f0 = funcompose(test_f,test_g);
[s,r,t,v,z,q,m,n] = deal(2.2,17,2*rand(1),4,2,5*rand(1),1,11);
[o_,k_,h_,b_,a_,f_,c_,e_] = f0(s,r,t,v,z,q,m,n);
[b a c f e] = funcompose_test_g(s,r,t,v,z);
[o k h] = funcompose_test_f(q,r,e,t,m,n,c);
comparison.two = [o o_;k k_;h h_;b b_;a a_;f f_;c c_;e e_];
%% Test composition of three functions
test_d = at('funcompose_test_d');
f2 = funcompose(test_f,{test_g,test_d});
[l,p,t,v,z,c] = deal(4.1,2.2,pi,0.1,-5,-7);
[v,s,r,m,n] = funcompose_test_d(l,p);
[b a c f e] = funcompose_test_g(s,r,t,v,z);
[o k h] = funcompose_test_f(q,r,e,t,m,n,c);
[o_ k_ h_] = f2(l,p,t,z,q);
comparison.three = [o o_;k k_;h h_];
%% Test composition of four functions
test_w = at('funcompose_test_w');
f3 = funcompose(test_w,{test_f,test_d,test_g});
[l,p,t,z,q,u] = deal(7,4,5*rand(1),6,3,rand(1));
[v,s,r,m,n] = funcompose_test_d(l,p);
[b a c f e] = funcompose_test_g(s,r,t,v,z);
[o k h] = funcompose_test_f(q,r,e,t,m,n,c);
[w_,x_] = funcompose_test_w(k,c,u,f);
[w, x] = f3(l,p,t,z,q,u);
comparison.four = [w w_;x x_];
function [seq] = toposort(adj)
% TOPOSORT A Topological ordering of nodes in a directed graph
%
% [SEQ] = TOPOSORT(ADJ)
%
% Inputs :
% ADJ : Adjacency Matrix.
% ADJ(i,j)==1 ==> there exists a directed edge
% from i to j
%
% Outputs :
% SEQ : A topological ordered sequence of nodes.
% empty matrix if graph contains cycles.
%
% Usage Example :
% N=5;
% [l,u] = lu(rand(N));
% adj = ~diag(ones(1,N)) & u>0.5;
% seq = toposort(adj);
%
%
% Note :
% See also
% Uses :
% Change History :
% Date Time Prog Note
% 18-May-1998 4:44 PM ATC Created under MATLAB 5.1.0.421
% ATC = Ali Taylan Cemgil,
% SNN - University of Nijmegen, Department of Medical Physics and Biophysics
% e-mail : cemgil@mbfys.kun.nl
N = size(adj);
indeg = sum(adj,1);
outdeg = sum(adj,2);
seq = [];
for i=1:N,
% Find nodes with indegree 0
idx = find(indeg==0);
% If can't find than graph contains a cycle
if isempty(idx),
seq = [];
break;
end;
% Remove the node with the max number of connections
[dummy idx2] = max(outdeg(idx));
indx = idx(idx2);
seq = [seq, indx];
indeg(indx)=-1;
idx = find(adj(indx,:));
indeg(idx) = indeg(idx)-1;
end
function [U,varargout] = private_union(varargin)
%function [U,varargout] = private_union(varargin)
%
% varargin Listing of cell arrays of char, each representing a set
% U cell array representing union of varargin{1},...,varargin{end}
% varargout respective indexes into the union so that varargin{j} = U(varargout{j})
if ischar(varargin{1}) && strcmpi(varargin{1},'test'),
[U,varargout{1},varargout{2},varargout{3},varargout{4}] = test_private_union;
else
U = {};
for j=1:length(varargin),
U = union(U,varargin{j});
end
for j=1:length(varargin),
S = varargin{j};
if ~isempty(S),
nEntries = length(S);
I = zeros(nEntries,1);
for k=1:nEntries,
[ignoreMe,I(k)] = ismember(S{k},U);
end
varargout{j} = I;
else
varargout{j} = [];
end
end
end
function [U,a,b,c,d] = test_private_union
A = {'x','y','z'};
B = {'x','a','gamm','bet'};
C = {};
D = {'z','r','s','t'};
[U,a,b,c,d] = private_union(A,B,C,D);