Mianzhi Wang

Ph.D. in Electrical Engineering

Closures in MATLAB


Introduction

Nested functions and anonymous functions provide great flexibility when coding in MATLAB. They also make it possible to create closures (I mean the closures in computer programming, not mathematics) in MATLAB, leading to some interesting use cases. In simple words, a closure captures a persistent local variable scope, which usually occurs when your function A returns a function B that references local variables in function A. In MATLAB, if you write the some code similar to the code below, you create a closure:

function g = scale_operator(f, scale)
    % Here we assume f has a single return value, and scale is also a scalar
    function y = scaled_function(varargin)
        % Note that we use the local variable scale and f here, which is within
        % the scope of scale_operator().
        y = scale * f(varargin{:});
    end
    g = @scaled_function;
end

When you call the returned function, it will access the local variables f and scale captured in the closure.

You may wonder why it bothers to write such "confusing" code to make a function return a function. Interestingly, the above code implements a simple scale operator (here I mean the operator in mathematics, not computer programming) that maps functions with their scaled versions. Take a look at the following code and you will get a better idea:

f = @(x) x.*x; % just a simple quadratic function
g = scale_operator(f, 2); % g(x) = 2*f(x)
h = scale_operator(g, 3); % h(x) = 3*g(x)
disp(f([1 2 3])); % [1 4 9]
disp(g([1 2 3])); % [2 8 18]
disp(h([1 2 3])); % [6 24 54]

Interesting? Let us continue.

The Light Side

In addition to the above example, there are many other neat applications that utilize closures in MATLAB. The most straightforward application is parameterizing functions. Basically, you create a function that works like a factory that creates new functions depending on the input parameters. The example below shows a quadratic function factory. It will create a new quadratic function with every set of new coefficients.

function qf = create_quadratic_function(a, b, c)
    function y = quadratic_function(x)
        y = a*x.^2 + b*x + c;
    end
    qf = @y;
end

If you are an one-linear, you can also define the above function using the syntax of anonymous functions:

create_quadratic_function = @(a,b,c) @(x) a*x.^2 + b*x + c;

However, when you need to perform complex operation with respect to the parameters, you may need to use the syntax of nested functions. Use the syntax of anonymous functions only when it is clear and concise.

Now we can create new quadratic functions and use them just as normal MATLAB functions:

x = linspace(-2, 2, 100);
q1 = create_quadratic_function(1, 1, 1);
q2 = create_quadratic_function(1, -1, 1);
plot(x, q1(x), x, q2(x));

Running the code above and you show get the following plot:

quadratic functions

Another interesting use case of closures arises when you want your function to cache the results from complex computations, and you do not want to pollute the global workspace. Consider the following function that "solves the ultimate question":

function get_answer = create_ultimate_solver()
    solved = false;
    cached_answer = [];
    function answer = solve()
        if ~solved
            % assuming it will take a long time to obtain this answer
            cached_answer = 42;
            solved = true;
            disp('Solved! The answer is cached.');
        else
            disp('Already solved! Using the cached answer.');    
        end
        answer = cached_answer;
    end
    get_answer = @solve;
end

When you evaluate the returned get_answer() function multiple times, it will not repeat complex computations multiple times. It will only perform the complex computations the first time you call it, and use cached results later on, as shown below:

get_answer = create_ultimate_solver();
answer1 = get_answer(); % 42, 'Solved! The answer is cached.'
answer2 = get_answer(); % 42, 'Already solved! Using the cached answer.'
answer2 = get_answer(); % 42, 'Already solved! Using the cached answer.'

With closures, it is also possible to mimic an object instance using structs. Note that this trick does not replace the OOP programming in MATLAB, but the resulting struct will have field that looks like methods in OOP programming.

The idea is quite simple. We utilize closures to store private local variables. We use nested functions to manipulate these variables and connect them with the outside. We then use a MATLAB struct to store handles to these nested functions, so the struct will look like a object instance. Here is an example function that creates a counter:

function ctr = create_counter(n)
    if nargin < 1
        n = 0;
    end
    function increase()
        n = n + 1;
    end
    function decrease()
        n = n - 1;
    end
    function reset()
        n = 0;
    end
    function set_count(m)
        n = m;
    end
    function x = get_count()
        x = n;
    end
    ctr.increase = @increase;
    ctr.decrease = @decrease;
    ctr.reset = @reset;
    ctr.set_count = @set_count;
    ctr.get_count = @get_count;
end

The returned counter struct will have five "methods". We can increase/decrease the count by calling ctr.increase()/ctr.decrease(), reset the counter by calling ctr.reset(), get/set the current count by calling ctr.get_count() /ctr.set_count(), as shown below:

ctr1 = create_counter();
ctr2 = create_counter(10);
disp(ctr1.get_count()); % 0
disp(ctr2.get_count()); % 10
ctr1.increase();
ctr2.decrease();
disp(ctr1.get_count()); % 1
disp(ctr2.get_count()); % 9

Here each created counter has it own closure to store the count variable, and they are independent of each other.

Similarly, we can create a stateful sine wave generator as follows, which stores the current phase in the corresponding closure:

function wg = create_sine_wave_generator(f, fs)
    % Here f is the frequency of the sine wave, and fs is the sampling frequency.
    phase = 0;
    function y = next_samples(n)
        tmp = 2 * pi * f * (0:n - 1) / fs + phase;
        y = sin(tmp);
        phase = phase + 2 * pi * f * n / fs;
    end
    function reset()
        phase = 0;
    end
    wg.next_samples = @next_samples;
    wg.reset = @reset;
end

Now we can create a new sine wave generator and generate some samples as follows:

wg = create_sine_wave_generator(1, 50);
y1 = wg.next_samples(20); % get the next 20 samples
y2 = wg.next_samples(20); % get the next 20 samples
wg.reset(); % reset
y3 = wg.next_samples(20); % get the next 20 samples
stem(1:20, y1); hold on;
stem(21:40, y2); hold on;
stem(41:60, y3); hold off;

Running the above code you will get the plot above. It can be observed that the new sine wave generator is indeed stateful.

stateful waveform generator

The Dark Side

While closures are powerful, we need to be cautious when using them. Here are three things to remember when your MATLAB code involves closures:

  1. Closures may cause memory leaks. Things get worse when you define a function that returns a function that returns a function that ... Additionally, because the local variables captured in closures are not visible in the global workspace. You cannot not use clear varname to clear them directly, and you may forget to clear them. To clear them, you need to clear the returned function handle (e.g., g and h in the example at the very beginning) associated with their closure.

  2. There is a crucial difference between anonymous functions and nested functions when capturing the local variables:

    • When an anonymous function is created, the immediate values of the referenced local variables will be captured. Hence if any changes to the referenced local variables made after the creation of this anonymous function will not affect this anonymous function.
    • When a nested function is created, the immediate values of the referenced local variables will not be captured. When the nested function is called, it will use the current values of the referenced local variables.

    To get a better understanding, consider the following two functions:

    function [f1, f2] = create_functions_anonymous()
        ii = 1;
        f1 = @(x) x * ii;
        ii = 2;
        f2 = @(x) x * ii;
    end
    
    function [f1, f2] = create_functions_nested()
        ii = 1;
        function y = function_1(x)
            y = x * ii;
        end
        f1 = @function_1;
        ii = 2;
        function y = function_2(x)
            y = x * ii;
        end
        f2 = @function_2;
    end
    

    If you run the code below, you will observe the difference.

    [f1, f2] = create_functions_anonymous();
    disp(f1(9)); % 9
    disp(f2(9)); % 18
    
    [f1, f2] = create_functions_nested();
    disp(f1(9)); % 18
    disp(f2(9)); % 18
    
  3. When using parfor or similar parallel processing functions, local variables captured in closures will be copied to each worker, instead of being shared among all the workers. This will lead to unexpected results. Consider the following code:

    results = zeros(8, 4);
    ctr = create_counter(); % see the example above on how to create a counter
    parfor ii = 1:8
        for kk = 1:4
            ctr.increase();
            results(ii,kk) = ctr.get_count();
        end
    end
    disp(results);
    

    If you run the above code with 4 MATLAB worker, you will get very confusing results which look like this:

    5     6     7     8
    1     2     3     4
    5     6     7     8
    1     2     3     4
    1     2     3     4
    1     2     3     4
    5     6     7     8
    9    10    11    12
    

    Actually, you will get difference results each time you run the above code. This occurred because ctr and its closure will be copied to the 4 workers, so each work will have a counter that is independent of the counters possessed by other workers. To make matters worse, there is no guarantee how MATLAB distributes the tasks among the workers. Hence the results will be different each time.

Summary

In summary, there are many neat trick you can do with closures in MATLAB, such as defining operators, parameterizing functions, cache local results, mimic object instances, etc. However, you must be careful when using them to avoid memory leaks and unexpected results. If you are interested, you may also check out functional programming.