Loren on the Art of MATLAB

Turn ideas into MATLAB

Introduction to Functional Programming with Anonymous Functions, Part 2 14

Posted by Loren Shure,

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.

Last time, we said that functional programming was marked by storing functions as variables (function handles) and working with functions that act on other functions. We put these ideas together to implement our own version of a map function for handling multiple inputs and outputs from multiple functions simultaneously, and we created iif, an "inline if", to allow the use of conditional statements inside of anonymous functions. So how might we work with recursive functions -- functions of themselves? We'll see how a functional programming style allows us to implement recursive functionality inside anonymous functions, and this will pave the way for the final part, in which we'll implement loops, without ever using for or while (which we can't use in anonymous functions).

Before we get started, let's implement iif again; we're going to need it frequently.

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

Anonymous Function Recursion

Recall that a recursive function is a function that calls itself. It therefore needs some way to refer to itself. When we write an anonymous function, it isn't "named" (hence, "anonymous"), so it can't call itself by name. How can we get around this?

Let's start with a Fibonacci sequence example. Recall that the nth number of the Fibonacci sequence is the sum of the previous two numbers, starting with 1 and 1, yielding 1, 1, 2, 3, 5, 8, 13, 21, etc. This is easy to implement recursively.

   fib = @(n) iif(n <= 2, 1, ...                    % First two numbers
                  true,   @() fib(n-1) + fib(n-2)); % All later numbers

But hey, that can't work! We haven't defined fib yet, so how could this anonymous function call it? In fact, the anonymous function will never "know" we're referring to it as fib, so this won't work at all. Therefore, instead of trying to call fib directly, let's provide another input: the handle of a function to call, f.

fib = @(f, n) iif(n <= 2, 1, ...                      % First two numbers
                  true,   @() f(f, n-1) + f(f, n-2)); % All later numbers

Getting closer. Now, if we pass fib to fib along with the number we want, it will call fib, passing in fib as the first argument, recursively until we get our answer.

fib(fib, 6)
ans =
     8

Ok, that's right. The sixth number of the sequence is 8. On the other hand, the syntax we've created is terrible. We have to provide the function to itself? I'd rather not. Instead, let's just write a new function that hands fib to fib along with the input n.

fib2 = @(n) fib(fib, n);

fib2(4)
fib2(5)
fib2(6)
ans =
     3
ans =
     5
ans =
     8

That's a lot closer to what we want, but there's one more step. Let's write a function called recur to hand a function handle to itself, along with any other arguments. This makes recursion less cumbersome.

recur = @(f, varargin) f(f, varargin{:});

That was simple, so now let's re-write fib. The first argument to recur is the function, which we'll define inline. The second is n. That's all there is to it. It now reads as "Recursively call a function that, if k <= 2, returns one, and otherwise returns the recursive function of k-1 plus that of k-2, starting with the user's input n." (If it doesn't read quite this clearly at first, that's ok. It takes some getting used to. Comment liberally if necessary!)

fib = @(n) recur(@(f, k) iif(k <= 2, 1, ...
                             true,   @() f(f, k-1) + f(f, k-2)), ...
                 n);

And we can find the first ten numbers of the sequence via arrayfun.

arrayfun(fib, 1:10)
ans =
     1     1     2     3     5     8    13    21    34    55

Factorial (f(n) = 1 * 2 * 3 * ... n) is another easy operation to represent recursively.

factorial = @(n) recur(@(f, k) iif(k == 0, 1, ...
                                   true,   @() k * f(f, k-1)), n);
arrayfun(factorial, 1:7)
ans =
  Columns 1 through 6
           1           2           6          24         120         720
  Column 7
        5040

A number to an integer power has a nearly identical form. Here's 4.^(0:5).

pow = @(x, n) recur(@(f, k) iif(k == 0, 1, ...
                                true,   @() x * f(f, k-1)), n);
arrayfun(@(n) pow(4, n), 0:5)
ans =
           1           4          16          64         256        1024

That was a big step for anonymous functions, using both recursion and an inline conditional together with ease. Like map and iif, recur, looks strange at first, but once it's been seen, it's hard to forget how it works (just make one of the inputs a function handle and pass it to itself). And recursion doesn't have to stop at interesting mathematical sequences of numbers. For instance, in the next part, we'll use this to implement loops in, but first, we'll need a some helper functions and a good way to execute multiple statements in an anonymous function.

Helpers

These little functions are useful in many circumstances, and we're going to need curly frequently.

paren = @(x, varargin) x(varargin{:});
curly = @(x, varargin) x{varargin{:}};

They allow us to write x(3, 4) as paren(x, 3, 4) and similarly for curly braces. That is, now we can think of parentheses and curly braces as functions! At first this might not seem useful. However, imagine writing a function to return the width and height of the screen. The data we need is available from this call:

get(0, 'ScreenSize')
ans =
           1           1        1920        1200

However, we don't need those preceeding ones. We could save the output to a variable, say x, and then access x(3:4), but if we need this in an anonymous function, we can't save to a variable. How do we access just elements 3 and 4? There are numerous ways, but paren and curly are similar to constructs found in other languages and are easy to use, so we'll use those here.

Now we can write our screen_size function to return just the data we want.

screen_size = @() paren(get(0, 'ScreenSize'), 3:4);

screen_size()
ans =
        1920        1200

While on the subject, note that we can actually use any number of indices or even ':'.

magic(3)
paren(magic(3), 1:2, 2:3)
paren(magic(3), 1:2, :)
ans =
     8     1     6
     3     5     7
     4     9     2
ans =
     1     6
     5     7
ans =
     8     1     6
     3     5     7

We do the same with the curly braces. Here, the regular expression pattern will match both 'rain' and 'Spain', but we'll only select the second match.

spain = curly(regexp('The rain in Spain....', '\s(\S+ain)', 'tokens'), 2)
spain = 
    'Spain'

(Click for Regexp help.)

It also works with ':' (note that the single quotes are required).

[a, b] = curly({'the_letter_a', 'the_letter_b'}, ':')
a =
the_letter_a
b =
the_letter_b

Executing Multiple Statements

With curly in place, let's examine something a little different. Consider the following:

do_three_things = @() {fprintf('This is the first thing.\n'), ...
                       fprintf('This is the second thing.\n'), ...
                       max(eig(magic(3)))};

do_three_things()
This is the first thing.
This is the second thing.
ans = 
    [25]    [26]    [15]

We've executed three statements on a single line. All of the outputs are stored in the cell array, so we have three elements in the cell array. The first two outputs are actually garbage as far as we're concerned (they're just the outputs from fprintf, which is the number of bytes written, which we don't care about at all). The last output is from max(eig(magic(3))); That is, the biggest eigenvalue of magic(3) is exactly 15. Let's say we just wanted that final value, the eigenvalue. It's the third element of the cell array, so we can grab it with curly.

do_three_things = @() curly({fprintf('This is the first thing.\n'), ...
                             fprintf('This is the second thing.\n'), ...
                             max(eig(magic(3)))}, 3);

do_three_things()
This is the first thing.
This is the second thing.
ans =
           15

For a more complex example, let's say we want to write a function to:

  1. Create a small figure in the middle of the screen
  2. Plot some random points
  3. Return the handles of the figure and the plot

Then by storing all of the outputs in a cell array and using curly to access the outputs we care about, we can make a multi-line function with multiple outputs, all in a simple anonymous function.

dots = @() curly({...
    figure('Position', [0.5*screen_size() - [100 50], 200, 100], ...
           'MenuBar',  'none'), ...                % Position the figure
    plot(randn(1, 100), randn(1, 100), '.')}, ...  % Plot random points
    ':');                                          % Return everything

[h_figure, h_dots] = dots()
h_figure =
     3
h_dots =
          187

(As a quick aside, note that if a statement doesn't return anything, we can't put it in a cell array, and so we can't use it this way. There are ways around this, discussed here.)

To Be Continued

Today, we've come a long way, from a simple condition through recursion and executing multiple statements. Here's a roundup of the functions so far.

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{:}};

These can also be found here, implemented as regular MATLAB functions that can be kept on the path.

Next time, we'll look at loops. Until then, have you worked with functions such as paren or curly? How else are people implementing these or similar operations? Let us know here.


Get the MATLAB code

Published with MATLAB® R2012b

168 views (last 30 days)  | |

Comments

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