Loren on the Art of MATLAB

Turn ideas into MATLAB

Note

Loren on the Art of MATLAB has been archived and will not be updated.

Introduction to Functional Programming with Anonymous Functions, Part 3

Tucker McClure is an Application Engineer with The MathWorks. He spends his time helping our customers accelerate their work with the right tools and problem-solving techniques. Today, he'll be discussing how "functional programming" can help create brief and powerful MATLAB code.

Contents

Recap

For Part 1, click here. For Part 2, click here.

When we left off, we had implemented conditional statements, recursion, and multi-line statements in anonymous functions, so today we'll tackle loops.

Before we get started, let's implement the functions that we'll need again.

iif     = @(varargin) varargin{2*find([varargin{1:2:end}], 1, 'first')}();
recur   = @(f, varargin) f(f, varargin{:});
curly   = @(x, varargin) x{varargin{:}};

Loops

Note that the recursive sequences we created in the last part could also have been implemented with for loops. For instance, here's factorial of n:

   factorial = 1;
   for k = 1:n
       factorial = k * factorial;
   end

Many times, recursive functions can be written iteratively in loops. However, we can't use for or while in an anonymous function, so instead of asking how we can unwrap recursive functions into iterative loops, let's ask the reverse: how can we implement loops with recursive functions?

Loops via Recursion

To loop properly, one must know:

  • What to do each iteration
  • If the process should continue to the next iteration
  • What's available when the loop begins

Allowing the "what to do" to be a function (fcn) of some state (x), the "if it should continue" to be another function (cont) of the state, and "what's available when the loop begins" to be the initial state (x0), we can write a loop function. This is a big step, so bear with me for some explanation!

On each step, the loop function will call the cont function, passing in all elements of the state, x, as in cont(x{:}). If that returns false (meaning we shouldn't continue), the current state, x, is returned. Otherwise, if we should continue, it calls fcn with all elements of the current state, as in fcn(x{:}), and passes the output from that to the next iteration. Letting this single iteration be denoted as f, we can build the anonymous function loop using our recur function.

loop = @(x0, cont, fcn) ...                                     % Header
       recur(@(f, x) iif(~cont(x{:}), x, ...                    % Continue?
                         true,        @() f(f, fcn(x{:}))), ... %   Iterate
             x0);                                               % from x0.

For this trivial example, the state is simply the iteration count. We'll increase the count every iteration until the count >= n and return the final count. All this does therefore is count from 0 to the input n. Not very interesting, but it demonstrates the loop.

count = @(n) loop({0}, ...          % Initialize state, k, to 0
                  @(k) k < n, ...   % While k < n
                  @(k) {k + 1});    %   k = k + 1 (returned as cell array)

arrayfun(count, 1:10)
ans = 
    [1]    [2]    [3]    [4]    [5]    [6]    [7]    [8]    [9]    [10]

I suppose that worked, but why are we using cell arrays to store the state, such as {0} and {k+1}? There are two reasons. First, if x is a cell array, then when we dump all elements of x into fcn, they become multiple arguments! That is, fcn(x{:}) is the same as fcn(x{1}, x{2}, ...). So instead of our function taking a big cell array for an input, it can take named arguments, which we'll use below. Second, we do this because it allows a function to return multiple elements that will be used by the next iteration, so if a function needed to return y and z, which would be arguments to the next iteration, it can simply return one cell array, {y, z}. It makes it easy to use. Here's a factorial example demonstrating this. The state is two different things: the iteration count, k, and factorial of the previous number, x. Note that both values of the state, k and x, are inputs to all of the functions. Note here how we're using @(k, x) for our functions. By allowing x to be a cell array, each element of the array becomes an argument such as k or x!

factorial = @(n) loop({1, 1}, ...          % Start with k = 1 and x = 1
                      @(k, x) k <= n, ...  % While k <= n
                      @(k, x) {k + 1, ...  %   k = k + 1;
                               k * x});    %   x = k * x;

Call it:

factorial(5)
ans = 
    [6]    [120]

Wait, we wanted 120 (the fifth number of the factorial sequence), so what's that 6 doing there?

A Better Loop

Remember how we return the full state? That's not very useful for this factorial example, as we get both k and the number we want as outputs in that cell array. Because the whole state isn't generally useful, let's add a cleanup function to our loop. We'll execute this when the loop is done (right after ~cont(...) returns false). Our cleanup function will take the full state and return only the important parts.

loop = @(x0, cont, fcn, cleanup) ...                            % Header
       recur(@(f, x) iif(~cont(x{:}), cleanup(x{:}), ...        % Continue?
                         true,        @() f(f, fcn(x{:}))), ... %   Iterate
             x0);                                               % from x0.

Now here's factorial, with clean output.

factorial = @(n) loop({1, 1}, ...         % Start with k = 1 and x = 1
                      @(k,x) k <= n, ...  % While k <= n
                      @(k,x) {k + 1, ...  %   k = k + 1;
                              k * x}, ... %   x = k * x;
                      @(k,x) x);          % End, returning x

The result:

factorial(5)
ans =
   120

First seven numbers of factorial:

arrayfun(factorial, 1:7)
ans =
  Columns 1 through 6
           1           2           6          24         120         720
  Column 7
        5040

That's better.

I'll be the first to admit that the loop is a bit longer and much more rigid than a normal MATLAB loop. On the other hand, it can be used in anonymous functions, and its syntax has a certain cleanliness to it in that it doesn't modify any variables that live outside the loop; it has its own scope. This is one nice feature of loop being a function that takes code (functions) as arguments.

Doing More in a Loop

Let's say we want to do something else in the loop, but don't want its output passed to the next iteration, like printing something out. Remember the do_three_things example from last time? We executed numerous statements by putting them in a cell array and used curly to access the output we cared about. We can do that here, in a loop. For example, let's write out a function to print n digits of the factorial sequence. We'll use an array to store two things. The first will be the number that fprintf returns, which we don't care about. The second will be the cell array we want to return, k and x. We'll access that cell array with curly, as in curly({..., {k, x}}, 2), which just returns {k, x}.

say_it = @(k, x) fprintf('Factorial(%d): %d\n', k, x);
print_factorial = @(n) loop({1, 1}, ...               % Start with k=1, x=1
                      @(k,x) k <= n, ...              % While k <= n
                      @(k,x) curly({say_it(k,k*x),... % Print, discard
                                    {k + 1, ...       %   k = k + 1;
                                     k * x}}, ...     %   x = k * x;
                                   2), ...            %   Return {k+1,k*x}.
                      @(k,x) x);                      % End, returning x
print_factorial(7);
Factorial(1): 1
Factorial(2): 2
Factorial(3): 6
Factorial(4): 24
Factorial(5): 120
Factorial(6): 720
Factorial(7): 5040

Now we're executing multiple things and only returning what we want while inside a loop built built on recursion and anonymous conditionals! We've come a long way since Part 1.

As a practical note, recall that because these loops use recursion, there's a limit to the number of times they can loop (MATLAB has a recursion limit, which is a setting in Preferences). Also, a recursive implementation of a loop isn't the most efficient. For this reason, it's best to implement loop itself in a file that can then be used in the same way. If it's in a file, it can also be kept on the MATLAB path so that it can be used anywhere.

   function x = loop(x, cont, f, cleanup)
       while cont(x{:})
           x = f(x{:});
       end
       if nargin == 4
           x = cleanup(x{:});
       end
   end

Final Example

This brings us to our final example. Below, we'll simulate a simple harmonic oscillator over time, using a structure to store dissimilar states, including a complete time history of the oscillator. This might simulate, for example, the sway of a lamp that's hanging from the ceiling after an earthquake.

% First, calculate a state transition matrix that represents a harmonic
% oscillator with damping. Multiplying this by |x| produces |x| at a
% slightly later time. The math here isn't important to the example.
Phi = expm(0.5*[0 1; -1 -0.2]);

% Now create the loop.
x   = loop({[1; 0], 1}, ...                  % Initial state, x = [1; 0]
           @(x,k) k <= 100, ...              % While k <= 100
           @(x,k) {[x, Phi * x(:, end)], ... %   Update x
                   k + 1}, ...               %   Update k
           @(x,k) x);                        % End, return x

% Create a plot function.
plot_it = @(n, x, y, t) {subplot(2, 1, n), ...            % Select subplot.
                         plot(x(n, :)), ...               % Plot the data.
                         iif(nargin==4, @() title(t), ... % If there's a
                             true,      []), ...          % title, add it.
                         ylabel(y), ...                   % Label y
                         xlabel('Time (s)')};             % and x axes.

% Plot the result.
plot_it(1, x, 'Position (m)', 'Harmonic Oscillator');
plot_it(2, x, 'Velocity (m/s)');

Summary

That's it for loops via recursion!

Let's look back at what we did over these three parts. First, we started with a simple map utility function to demonstrate the function-of-functions idea. Then we created our ubiquitous inline if, which further enabled recursion (a conditional is necessary to make recursion stop!). We also showed using multiple statements by storing their outputs in a cell array. Finally, we created a loop construct on top of our recursion functions.

At this point, we've done more than just scratch the surface of functional programming. We've used MATLAB's interesting constructs, such as function handles, cell arrays, and varargin to implement a functional programming framework, allowing a new syntax within MATLAB, where code can be arguments to flow control functions. Here's a roundup of what we created.

map   = @(val, fcns) cellfun(@(f) f(val{:}), fcns);
mapc  = @(val, fcns) cellfun(@(f) f(val{:}), fcns, 'UniformOutput', 0);
iif   = @(varargin) varargin{2*find([varargin{1:2:end}], 1, 'first')}();
recur = @(f, varargin) f(f, varargin{:});
paren = @(x, varargin) x(varargin{:});
curly = @(x, varargin) x{varargin{:}};
loop  = @(x0,c,f,r)recur(@(g,x)iif(c(x{:}),@()g(g,f(x{:})),1,r(x{:})),x0);

These have also been programmed as "normal" MATLAB functions so that they can be kept on the path and used whenever they're needed. These can be found under "Functional Programming Constructs" in File Exchange, here.

Thanks for reading. I hope this has both enabled a new level of detail in anonymous functions in MATLAB and helped demonstrate the wide range of possibilities available within the MATLAB language.

Do you have other functional programming patterns you use in your code? For instance, a do-while loop is just like our loop above except that it always runs at least one iteration. Any ideas how to program this or other interesting constructs in anonymous functions? Please let us know here!




Published with MATLAB® R2012b


  • print

Comments

To leave a comment, please click here to sign in to your MathWorks Account or create a new one.