Technical Articles

Dynamic Function Creation with Anonymous and Nested Functions

By Stuart McGarrity, MathWorks and Loren Shure, MathWorks


When you are developing algorithms to solve technical computing problems, it is often useful to create functions on-the-fly so that you can customize them at run-time without having to define them in files beforehand. For example:

  • You may want to create a function based on a user's input and then evaluate or plot it over a specific range.
  • You may want to define a function and then pass it as an argument to an optimization, numerical integration, or ODE solving routine (also known as a function function in MATLAB).
  • In curve fitting, you need to create a function that best fits some chosen data and subsequently need to evaluate it at other data points or perform further operations on it, such as integration.
  • Sometimes it is necessary to create a function that needs to retain dynamic state and which must be initialized the first time it is called and updated at each subsequent call.

Other uses for dynamically created functions include callbacks in MATLAB Handle Graphics, data acquisition objects and timers, and, generally, any time you need to pass a function as a parameter to another routine.

Earlier versions of MATLAB already provided a number of features for managing functions, but there were limitations, for example:

  • The MATLAB language has function handles that let you pass functions as parameters to routines, but they needed to be associated with a function definition in an M-file and evaluated with the feval command.
  • You can specify arbitrary functions as a string with the inline command and pass them to the function function routines, but this command is difficult to use, particularly to separate the many different types of parameters, including the arguments to the inlined function and the function function itself.
  • The keyword persistent provides state in functions, but you can have only one instance of a traditional MATLAB function. Also, the function must carry out initialization the first time it is called.

Two new language features in MATLAB 7, anonymous functions and nested functions, address these issues and requirements for creating and managing functions.

Anonymous and Nested Functions in MATLAB 7

Using MathWorks tools, the researchers developed simple data acquisition code that could access a variety of data acquisition cards. “Data Acquisition Toolbox allows us to perform a one-time configuration of the hardware,” explains Vogt. “With less than a page of code, I could perform my data acquisition on four different platforms—each one with different hardware.”

The SSDK uses voltammetry, an advanced chemical analysis technique that translates chemical reactions into voltammetric “signature” outputs, enabling the detection of minute quantities of gaseous chemicals—something that was not previously possible.

The researchers implemented gaseous voltammetry using Data Acquisition Toolbox to output precisely shaped analog waveforms, each consisting of 100 to 1,000 points of data, and to excite chemical reactions on the microsensor. Using Data Acquisition Toolbox, they also sampled the resulting signals with an analog input channel. They then used filters from Signal Processing Toolbox to remove line noise. The resulting signals, or voltammetric signatures, included information that identified the chemicals to which the sensor was exposed.

The researchers used Neural Network Toolbox to perform pattern recognition and identify the chemicals by comparing their signatures to those in a prestored library.

“With Neural Network Toolbox, we can take an unknown sample signature, compare it to a signature library, find the best match, and calculate a confidence factor indicating how certain the match is,” Vogt says.

To generate the sensor signal-processing algorithms, Argonne used the Sensor Algorithm Generation Environment (ChemSAGE), a Department of Defense-funded software tool, written in MATLAB, that helps sensor researchers code and analyze complex signals from experimental sensors.

To complete their application, researchers developed a graphical user interface in MATLAB that enables users to set voltammetry parameters and visualize the voltammetric signatures. “The application runs easily on any notebook computer,” Vogt says.

Using MATLAB Compiler, Argonne recoded the MATLAB algorithms in C for execution on popular single-chip microcontrollers.

Argonne’s sponsors plan to develop commercial instruments based on SSDK, including intelligent fire detectors and air pollutant monitors.

Doctors Michael Vogt, Laura Skubal, Erika Shoemaker, and John Ziegler developed the SSDK. Vogt, Dan MacShane, Christopher Klaus, and Maria Poulos developed the measurement and ChemSAGE software.

Anonymous functions let you define a function on-the-fly at the command line or in M code, without an associated file. For example, to create a polynomial function,

y(x) = 3x2 + 2x + 1

type

y = @(x) 3*x.^2 + 2*x + 1;

This returns function handle y representing a function of one variable defined by the expression on the right. The variables in parentheses following the @ symbol (in this case, only x) specify variables of which y is a function. The function y can now be evaluated by calling it like any other function (another new feature in MATLAB 7).

y(3)

 ans =
       34

You could evaluate this polynomial over a range of points and plot the results together with the previously evaluated point.

x=-5:.1:5;
plot(x,y(x),'-b',4,y(4),'r*');
title('y(x) = 3*x^2 + 2*x + 1');
xlabel('x');
ylabel('y');
text(4,y(4),'y(4)');
grid 
df_graph1_w.gif
Click on image to see enlarged view.

We can create handles to functions of multiple variables.

g=@(x,y) (x.^2 + y.^2);
ezsurf(g);
shading flat;
title('f(x,y)=x^2+y^2'); 
df_graph2_w.jpg
Click on image to see enlarged view.

Using MathWorks tools, the researchers developed simple data acquisition code that could access a variety of data acquisition cards. “Data Acquisition Toolbox allows us to perform a one-time configuration of the hardware,” explains Vogt. “With less than a page of code, I could perform my data acquisition on four different platforms—each one with different hardware.”

The SSDK uses voltammetry, an advanced chemical analysis technique that translates chemical reactions into voltammetric “signature” outputs, enabling the detection of minute quantities of gaseous chemicals—something that was not previously possible.

The researchers implemented gaseous voltammetry using Data Acquisition Toolbox to output precisely shaped analog waveforms, each consisting of 100 to 1,000 points of data, and to excite chemical reactions on the microsensor. Using Data Acquisition Toolbox, they also sampled the resulting signals with an analog input channel. They then used filters from Signal Processing Toolbox to remove line noise. The resulting signals, or voltammetric signatures, included information that identified the chemicals to which the sensor was exposed.

The researchers used Neural Network Toolbox to perform pattern recognition and identify the chemicals by comparing their signatures to those in a prestored library.

“With Neural Network Toolbox, we can take an unknown sample signature, compare it to a signature library, find the best match, and calculate a confidence factor indicating how certain the match is,” Vogt says.

To generate the sensor signal-processing algorithms, Argonne used the Sensor Algorithm Generation Environment (ChemSAGE), a Department of Defense-funded software tool, written in MATLAB, that helps sensor researchers code and analyze complex signals from experimental sensors.

To complete their application, researchers developed a graphical user interface in MATLAB that enables users to set voltammetry parameters and visualize the voltammetric signatures. “The application runs easily on any notebook computer,” Vogt says.

Using MATLAB Compiler, Argonne recoded the MATLAB algorithms in C for execution on popular single-chip microcontrollers.

Argonne’s sponsors plan to develop commercial instruments based on SSDK, including intelligent fire detectors and air pollutant monitors.

Doctors Michael Vogt, Laura Skubal, Erika Shoemaker, and John Ziegler developed the SSDK. Vogt, Dan MacShane, Christopher Klaus, and Maria Poulos developed the measurement and ChemSAGE software.

Nested functions are functions in M-files that are nested inside other functions and which can see the parent function’s workspace. The following function taxDemo.m contains a nested function.

 function y = taxDemo(income)
% Calculate the tax on income.

AdjustedIncome = income - 6000; % Calculate adjusted income

% Call 'computeTax' without passing 'AdjustedIncome' as a parameter.

y = computeTax;
function y = computeTax
% This function can see the variable 'AdjustedIncome'
% in the calling function's workspace
y = 0.28 * AdjustedIncome;
end
end

The nested function computeTax can see the variables in the parent function's workspace, in this case AdjustedIncome and income. This makes sharing of data between multiple nested functions easy. We can call the function in the usual way.

  % What is the tax on income of 80,000?
 tax=taxDemo(80e3)

 tax =
       2.0720e+004

The ability of nested functions to see into their parent's workspace enables you to control the scope of your variables, letting them be accessed by multiple functions, while avoiding the side effects of us ing a global variable. At the same time, you can reduce your memory requirements by sharing large data sets in a controlled way.

An end statement is required at the end of all nested functions, making them different from traditional local subfunctions. However, all functions in a file containing nested functions, including traditional subfunctions, require termination with an end statement. Functions can be nested to any level.

Customizing Functions

You can create different variants or customized versions of the same function with anonymous functions or nested functions. For example, if we want to create a function like

y = ax2 + bx + c

and customize a, b, or c at runtime, we write

a=3; b=2; c=-10;
y1=@(x) a*x.^2 + b*x + c; 

Any variables in the function that are not listed in parentheses are taken from the calling workspace at the time of definition.

Now let's change a, b, or c and generate other customized functions. For example,

a=-5; b=1; c=20;
y2=@(x) a*x.^2 + b*x + c; 

Evaluate or plot these as before.

x=-5:.1:5;
plot(x, y1(x), x, y2(x));
legend('y_1(x)=3x^2+2x-10','y_2(x)=-5x^2+x+20');
xlabel('x');
ylabel('y_1 and y_2');
title('y(x) = a*x^2 + b*x + c');
grid
df_graph3_w.gif
Click on image to see enlarged view.

Using MathWorks tools, the researchers developed simple data acquisition code that could access a variety of data acquisition cards. “Data Acquisition Toolbox allows us to perform a one-time configuration of the hardware,” explains Vogt. “With less than a page of code, I could perform my data acquisition on four different platforms—each one with different hardware.”

The SSDK uses voltammetry, an advanced chemical analysis technique that translates chemical reactions into voltammetric “signature” outputs, enabling the detection of minute quantities of gaseous chemicals—something that was not previously possible.

The researchers implemented gaseous voltammetry using Data Acquisition Toolbox to output precisely shaped analog waveforms, each consisting of 100 to 1,000 points of data, and to excite chemical reactions on the microsensor. Using Data Acquisition Toolbox, they also sampled the resulting signals with an analog input channel. They then used filters from Signal Processing Toolbox to remove line noise. The resulting signals, or voltammetric signatures, included information that identified the chemicals to which the sensor was exposed.

The researchers used Neural Network Toolbox to perform pattern recognition and identify the chemicals by comparing their signatures to those in a prestored library.

“With Neural Network Toolbox, we can take an unknown sample signature, compare it to a signature library, find the best match, and calculate a confidence factor indicating how certain the match is,” Vogt says.

To generate the sensor signal-processing algorithms, Argonne used the Sensor Algorithm Generation Environment (ChemSAGE), a Department of Defense-funded software tool, written in MATLAB, that helps sensor researchers code and analyze complex signals from experimental sensors.

To complete their application, researchers developed a graphical user interface in MATLAB that enables users to set voltammetry parameters and visualize the voltammetric signatures. “The application runs easily on any notebook computer,” Vogt says.

Using MATLAB Compiler, Argonne recoded the MATLAB algorithms in C for execution on popular single-chip microcontrollers.

Argonne’s sponsors plan to develop commercial instruments based on SSDK, including intelligent fire detectors and air pollutant monitors.

Doctors Michael Vogt, Laura Skubal, Erika Shoemaker, and John Ziegler developed the SSDK. Vogt, Dan MacShane, Christopher Klaus, and Maria Poulos developed the measurement and ChemSAGE software.

Since the definitions of y1 and y2 included the values of a, b, and c at the time the function handles are created, they are robust to variations in the workspace such as changed or deleted values of a, b, or c.

a=100;
y1(3)

 ans =
       23

a=5000;
y1(3)

 ans =
       23

Let's see how we can carry out a similar task with nested functions by looking at makefcn.m, which contains a nested function.

 function fcn = makefcn(a,b,c)
% This function returns a handle to a customized version of 'parabola'.
% a,b,c specifies the coefficients of the function.

fcn = @parabola; % Return handle to nested function
function y = parabola(x)
% This nested function can see the variables 'a','b', and 'c'
y = a*x.^2 + b.*x + c;
end
end

When you call makefcn, it returns a function handle to the internal nested function, which has been customized according to the parameters passed to the parent function. For example,

f1 = makefcn(3,2,10);
f2 = makefcn(0,5,25);

We can evaluate these two different functions as before.

f1(2)

 ans =
       26

f2(2)

 ans =
       35

In general, anonymous functions are better for quick, on-the-fly simple function creation and customization. Nested functions are more suited to more complex function customization and management.

Working with Function Functions

You can pass nested and anonymous functions to optimization or integration routines, known as function functions in MATLAB. For example, to find the area under the curve f1 between 0 and 4, use

areaunder=quad(f1,0,4)

 areaunder =
       120.0000

By having the integrand f1 defined beforehand, you keep its arguments (such as the polynomial coefficients) separate from those of the integrator quad (such as the limits of integration). This can eliminate potential confusion.

We will plot f1 again together with a plot of the area just calculated.

x=-5:.1:5;
plot(x,f1(x));
hold on
x=0:.1:4;
area(x,f1(x),'Facecolor','g'); % You can evaluate f without feval
hold off
title('Area under f_1(x), between 0 and 4');
grid
df_graph4_w.gif
Click on image to see enlarged view.

Handling Dynamic Function State

If you need to create a function that retains dynamic state, you can use a nested function and store its state in the parent's workspace. Functions with dynamic state can represent algorithms that range from simple counters to components of a large system.

Simple Function State

Let's create a counter function that returns a number which increments each time it is called. Let's look at the function makecounter.m.

 function countfcn = makecounter(initvalue)
% This function returns a handle to a customized nested function 'getCounter'.
% initvalue specifies the initial value of the counter whose handle is
% returned.

currentCount = initvalue; % Initial value
countfcn = @getCounter; % Return handle to getCounter

function count = getCounter
% This function increments the variable 'currentCount', when it
% gets called (using its function handle).
currentCount = currentCount + 1;
count = currentCount;
end
end

When you call makecounter, it returns a handle to its nested function getCounter. getCounter is customized by the value of initvalue, a variable it can see via nesting within the workspace of makecounter. Let's make a couple of counter functions.

counter1 = makecounter(0); % Define counter initialized to 0
counter2 = makecounter(10); % Define counter initialized to 10

Here we have created two customized counters: one that starts at 0 and one that starts at 10. Each handle is a separate instance of the nested function and its calling workspace. Note counter1 does not take any parameters. We need to use the parentheses to invoke the function instead of looking at the function handle variable itself.

counter1Value=counter1()

 counter1Value =
       1

We can call the two functions independently as there are two separate workspaces kept for the parent functions. They remain in memory while the handles to their nested functions exist. Each time the counter functions are called, the associated variable currentCount is incremented.

counter1Value=counter1()

 counter1Value =
       2

counter2Value=counter2()

 counter2Value =
       11

Complex Function State

You can use nested functions to store a more complex dynamic state such as that of a filter. Let's look at the function makeFilter.m.

 function filterhandle = makeFilter(b,a)

% Initialize State
state=zeros(max(length(a),length(b))-1,1);

% Return handle to filter function
filterhandle=@callFilter;
function output = callFilter(input)
% Calculate output and update state
[output,state] = filter(b,a,input,state);
end
end

The function makeFilter returns a handle to a nested function (callFilter) that performs a filtering operation and maintains its state in the parent's workspace. The filter state is initialized when the function handle is created, then gets updated each time the nested function is called. The nested function also has to calculate the output of the filtering operation and return it as its output argument. You can call this function in a for-loop many times, as shown in simplesystem.m.

 %% System Parameters
frameSize = 1000;
sampleTime = 1e-3;

%% Initialize System Components and Component Parameters
filter1 = makeFilter([0.02,0,-0.23,0,0.49,0,-0.23,0,0.02],1);
filter2 = makeFilter([-0.05 0.13 0.83 0.13 -0.05],1);
randSource = makeRandSource(frameSize);
scope = makeScope(sampleTime,[-2 2],'My Scope');

%% Simulation Loop
for k = 1:100
signal1 = randSource(); signal2 = filter1(signal1); signal3 = filter2(signal2); scope(signal3)
end

The main for-loop in the simulation now becomes very clean and simple where the only variables passed between the functions (signal1, signal2, and signal3) are pure data, without being mixed up with other function parameters (such as filter coefficients) and state.

The makeFilter routine could be expanded to return more than one function, such as one to return the filter state, or even one to reset the filter. These function handles could be returned as multiple output arguments or as a structure, where each field is a function handle. The functions can then be called in a manner similar to evaluating the methods of an object.

Other Uses of Nested and Anonymous Functions

The function handling capabilities of nested and anonymous functions have many other uses.

Function Composition

You can create handles to functions of functions, allowing a very natural mathematical notation for function composition, such as

f=@(x) x.^2;
g=@(x) 3*x;
h=@(x) g(f(x)); % Equivalent to 3*(x^2)
h(3)

 ans =
       27

Memoization

We can store the previously computed return values of a function in a "table" for later use instead of recalculating values. This is helpful if the function is computationally intensive. Let's look at a function that stores computed values and makes them available for reuse.

  function f = memoize(F)
% One argument F, inputs testable with ==
% Scaler input to F

x = [];
y = [];
f = @inner;
function out = inner(in)
ind = find(in == x);
if isempty(ind)
out = F(in);
x(end+1) = in;
y(end+1) = out;
else
out = y(ind);
end
end
end

Here's how to use this function. First you create a "memoized" version of the function for which you want to remember the return values, for example, sin.

f = memoize(@sin)

 f =
       @memoize/inner

Let's call the function a few times.

f(pi/2)

 ans =
       1

f(pi/4)

 ans =
       0.7071

f(pi/8)

 ans =
       0.3827

The returned values, in this case sin(pi/2), sin(pi/4), and sin(pi/8) are stored. To see how this is working, let's use the functions command to inspect the state of the function handle f.

functionInfo = functions(f)

 functionInfo = 
      function:   'memoize/inner'
      type:       
'nested'
      file:       
[1x76 char]
      workspace:  
{[1x1 struct]}

functionInfo.workspace{1}

 ans = 
       f: @memoize/inner
       F: @sin
       x: [1.5708 0.7854 0.3927]
       y: [1 0.7071 0.3827]

Now if you request a previously computed result, such as for sin(pi/4), the value is taken from the table without a new value being added. Here you can see the stored workspace doesn't change.

f(pi/4)

 ans = 
       0.7071

functionInfo = functions(f);
functionInfo.workspace{1}

 ans = 
       f: @memoize/inner
       F: @sin
       x: [1.5708 0.7854 0.3927]
       y: [1 0.7071 0.3827]

 

Data Structures

Nested functions can be used to create data structures such as lists and trees.

Published 2005

Products Used