No BSD License  

Highlights from
Automated composition of function handles

from Automated composition of function handles by Peter Cotton
Automatically compose function handles

funcompose(f,F)
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);

Contact us at files@mathworks.com