Code covered by the BSD License  

Highlights from
functor

image thumbnail

functor

by

 

Automated composition of function handles

functor
classdef functor < function_handle_like
    
    % Functor is an anotated function (i.e. a function_handle with named input and output arguments)
    % Collections of functors can be automatically composed into new
    % functors, using functor.compose and/or functor.combine 
    % 
    % I sometimes find this useful for refactoring, debugging and design of
    % larger programs. Trace through functor.unitTests to see what is going on. 
    % 
    % Author: Peter Cotton
    
    
    properties(GetAccess='public',SetAccess='protected')
        argin;
        argout;
    end
    
    properties(GetAccess='public',SetAccess='public');
        name;
    end
    
    methods
        
        function f = functor(h,argin,argout,name)
            % argin, argout:  cell array of input and output argument names
            % name:  char name of functor
            % h      function handle
            f.handle = h;
            f.argin = argin;
            f.argout = argout;
            if nargin==4,
                f.name = name;
            else
                f.name = '';
            end
        end
        
        function f1 = compose(f,varargin)
            f1 = combine(f,varargin{:});
            % Reduce f1's output list to the original (ignore side effects)
            f1.argout = f.argout;
            f1 = functor(f1.to_handle,f1.argin,f.argout,f.name);
        end
        
        function f1 = combine(f,varargin)
            %function f1 = combine(f,F)
            %
            % f         functor
            % varargin  functors
            
            n = length(varargin);
            switch n,
                case 0,
                    f1 = f;
                case 1,
                    f1 = combineTwo(f,varargin{1});
                otherwise
                    f1 = combineMany(f,{varargin{:}});
            end
            
            
            function f1 = combineTwo(f,g)
                % Compose two functions. Outputs from g are fed into inputs of
                % f, wherever the names coincide.
                
                % 1. Figure out input and output arguments for f1
                fIn = f.argin;
                fOut = f.argout;
                gIn = g.argin;
                gOut = g.argout;
                gInOrOut = union(gOut,gIn);
                [~,I1] = setdiff(fIn,gInOrOut); fInExtra=fIn(sort(I1));      % maintain original argument order
                [~,I2] = setdiff(gOut,fIn);     gOutUnused=gOut(sort(I2));   % maintain original argument order
                [~,I3] = intersect(gOut,fIn);    gOutUsed=gOut(sort(I3)); % maintain original argument order
                [U,gIn_,gOut_,fIn_,fOut_,fInExtra_,gOutUnused_,gOutUsed_] = argumentUnion(gIn,gOut,fIn,fOut,fInExtra,gOutUnused,gOutUsed);
                argin_ = [gIn_;fInExtra_];
                f1_argin = U(argin_);
                argout_ = [fOut_;gOutUnused_;gOutUsed_];
                f1_argout = U(argout_);
                
                % 2. Construct new functor
                fh = f.to_handle;
                gh = g.to_handle;
                f1h = @(varargin) jfeval(fh,gh,U,gIn_,fIn_,gOut_,fOut_,fInExtra_,gOutUnused_,gOutUsed_,varargin{:});
                f1 = functor(f1h,f1_argin,f1_argout);
                
                function [U,varargout] = argumentUnion(varargin)
                    %function [U,varargout] = argumentUnion(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})
                    
                    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,
                                [~,I(k)] = ismember(S{k},U);
                            end
                            varargout{j} = I;
                        else
                            varargout{j} = [];
                        end
                    end
                    
                end
                
                
                function varargout = 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
                    varargout = cell(1,length(cards));
                    
                    %padding = (n+1)*ones(24-length(cards),1);  V{n+1} = nan; %
                    %Pass nan to extraneous output argument
                    %varargout = cell(1,24);
                    
                    [varargout{:}] = deal(V{cards});
                    
                    function c = cfeval(f,nArgout,varargin)
                        % Return a cell array containing all arguments
                        c = cell(1,nArgout);
                        [c{1:nArgout}] = feval(f,varargin{:});
                    end
                    
                end
            end
            
            function f1 = combineMany(f,F)
                emty = cellfun(@isempty,F);
                F = F(~emty);
                A = intentGraph(F);
                seq = toposort(A');
                if isempty(seq),
                    error('funcompose. Cyclic dependency detected.');
                else
                    f1 = f;
                    for k=1:length(seq),
                        fk = F{seq(k)};
                        if isempty(intersect(f1.argin,fk.argout)),
                            % ignore fk
                        else
                            f1 = combineTwo(f1,fk);
                            f1.name = f.name;
                        end
                    end
                end
            end
            
            
            
            
            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
                    [~, idx2] = max(outdeg(idx));
                    indx = idx(idx2);
                    seq = [seq, indx];
                    indeg(indx)=-1;
                    idx = find(adj(indx,:));
                    indeg(idx) = indeg(idx)-1;
                end
            end
            
            function A = intentGraph(F)
                % F  nFunc x 1 cell array of functor
                % 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 = F{j}.argout;
                    for k1=1:nFun,
                        if j~=k1,
                            fkIn = F{k1}.argin;
                            A(j,k1) = ~isempty(intersect(fjOut,fkIn));
                        end
                    end
                end
                
            end
            
        end
        
        function g = to_cellIterator(f)
           gh = @(varargin) functor.iterateOverCell(f,varargin{:});
           g = functor(gh,f.argin,f.argout,f.name);
        end  
        
    end
    
    methods(Static,Access='public')
        
        function okay = unitTests
            results = [functor.unitTest_iterateOverCell;...
                functor.unitTest_constructor;...
                functor.unitTest_combine;...
                functor.unitTest_combineMany;...
                functor.unitTest_plot];
            okay = all(results);
        end
        
        function [labels,nodeType,adj] = view(varargin)
            
            parents = struct;
            children = struct;
            
            F = {varargin{:}};
            % Get rid of empty ones
            emty= cellfun(@isempty,F);
            F = F(~emty);
            
            nextFreeNode = 1;
            for k=1:length(F),
                f = F{k};
                parent = nextFreeNode;
                labels{parent} = f.name;
                nodeType(parent) = 2;
                nextFreeNode = nextFreeNode+1;
                parents.(f.name)=parent;
                % Add input arguments to adjacency graph
                for j=1:length(f.argin),
                    childName = f.argin{j};
                    if isfield(children,childName),
                        node = children.(childName);
                    else
                        node = nextFreeNode;
                        children.(childName) = node;
                        nextFreeNode = nextFreeNode+1;
                    end
                    nodeType(node) = 1;
                    labels{node} = childName;
                    adj(node,parent) = 1;
                end
                
                % Add output arguments to adjacency graph
                for j=1:length(f.argout),
                    childName = f.argout{j};
                    if isfield(children,childName),
                        node = children.(childName);
                    else
                        node = nextFreeNode;
                        children.(childName) = node;
                        nextFreeNode = nextFreeNode+1;
                    end
                    labels{node} = childName;
                    nodeType(node) = 1;
                    adj(parent,node) = 1;
                end
                
            end
            
            [a,b] = size(adj);
            minorAdj = adj;
            adj = zeros(nextFreeNode-1);
            adj(1:a,1:b) = minorAdj;
            
            bioPlot(labels,nodeType,adj);
            
        end
        
        
    end
    
    methods(Static,Access='private')  % Creating iterators
        
        function okay = unitTest_iterateOverCell
            
            % Direct
            f = functor(@(x,y,z) x+y+z{1},{'x','y','z'},{'q'},'f');
            X = {2,1,8};
            Y = 1;
            Z = {{7},{2},{1}};
            rslt = functor.iterateOverCell(f,X,Y,Z);
            knownRslt = {10,4,10};
            okay1 = isequal(rslt,knownRslt);
            
            % Indirect
            F = f.to_cellIterator;
            rsltCheck = feval(F,X,Y,Z);
            okay2 = isequal(rsltCheck,knownRslt);
            
            okay = okay1 && okay2;
            
        end
        
        function varargout = iterateOverCell(f,varargin)
            % Arguments are cell arrays of arguments
            
            % Verify
            nArgsIn = length(varargin);
            if length(f.argin)~=nArgsIn,
                error('Wrong number of input arguments'); 
            end
            if nargout>length(f.argout),
               error('Too many output arguments'); 
            end
            fh = f.to_handle;
            
            % Expand singleton arguments so they too are cell arrays
            % with the right number of elements        
            nElt = numberOfElements(varargin{1});
            for j=1:nArgsIn,
               nElt = max(nElt,numberOfElements(varargin{j})); 
            end
            for j=1:nArgsIn,
               nEltCheck=numberOfElements(varargin{j});
               if nEltCheck==1,
                   if iscell(varargin{j}),
                       elt = varargin{j}{1};
                   else
                       elt = varargin{j};
                   end
                   varargin{j} = cell(nElt,1);
                   for m=1:nElt,
                      varargin{j}{m} = elt; 
                   end
               elseif nEltCheck~=nElt,
                   error('functor.iterateOverCell anticipating all arguments to have the same number of elements');
               end
            end
            
            % Allocate
            varargout = cell(nargout,1);
            for j=1:nargout,
                varargout{j} = cell(size(varargin{1}));
            end
            
            % Compute
            for eltNo=1:nElt,
                f_argsIn=cell(nArgsIn,1);
                for k=1:nArgsIn,
                    v = varargin{k};
                    f_argsIn{k} = v{eltNo};
                end
                f_argOut=cell(nargout,1);
                if eltNo==1,
                    try
                       [f_argOut{:}] = feval(fh,f_argsIn{:});
                    catch ME
                       [f_argOut{:}] = feval(fh,f_argsIn{:});
                       error(['Could not create iterator for ',f.name,'. Call f(varargin{1}{1},varargin{2}{1},...) failed: ',ME.message]); 
                    end
                else
                    [f_argOut{:}] = feval(fh,f_argsIn{:});
                end
                
                for k=1:nargout,
                    varargout{k}{eltNo} = f_argOut{k};
                end
            end
            
            function n = numberOfElements(x)
                if ischar(x),
                    n = 1;
                else
                    n = numel(x);
                end
            end
            
        end
        
    end
    
    methods(Static,Access='private') % Unit tests
        
        function okay = unitTest_constructor
            f = functor(@sin,{'x'},{'y'});
            f(1);
            okay = true; % TODO: Fix test
        end
        
        function okay = unitTest_combine
            f = functor(@sin,{'x'},{'y'});
            g = functor(@cos,{'y'},{'z'});
            h = combine(g,f);
            val1 = feval(g,feval(f,0.5));
            [val2,sideEffect] = feval(h,0.5);
            okay = abs(val1-val2)<sqrt(eps);
        end
        
        function okay = unitTest_combineMany
            f = functor(@sin,{'x'},{'y'},'f');
            g = functor(@cos,{'y'},{'z'},'g');
            m = functor(@(y,z) y+z,{'y','z'},{'m'},'m');
            h1 = combine(g,f);
            val1 = feval(g,feval(f,0.5));
            [val2,sideEffect] = feval(h1,0.5);
            okay1 = abs(val1-val2)<sqrt(eps);
            
            h2 = combine(m,f,g);
            okay = okay1;
        end
        
        function okay = unitTest_plot
            f = functor(@sin,{'x'},{'y'},'F');
            g = functor(@cos,{'y'},{'z'},'G');
            m = functor(@(y,z) y+z,{'y','z'},{'m'},'M');
            r = functor(@(z,m) z-m,{'z','m'},{'r'},'R');
            functor.view(f,g,m,r);
            close all;
            okay = true;
        end
        
    end
    
    
end

Contact us