Today I wanted to talk about one specific custom class attribute that I have written and that showcases some of the beautiful things that you can achieve through the metaprogramming features of Matlab. If you haven’t yet, go through last post, in which I explain what custom attributes are and how to create them.
A brief intro into decorators
The definition of a decorator can be a bit confusing when you put it in words, but once you complement it with some code it clicks and starts to make sense. Check the following definition and the implementation in Matlab of a simple decorator:
function outFunction = uselessDecorator(inFunction)
% This decorator doesn't do anything!
arguments
inFunction (1,1) function_handle
end
outFunction = @(varargin) decorate(inFunction, varargin{:});
end
function varargout = decorate(fun, varargin)
% Just evaluate the function
[varargout{1:nargout}] = fun(varargin{:});
end
MATLABJust by looking at the interface of uselessDecorator()
we see that it matches the definition of a decorator: a function that takes one function (inFunction) and returns another one (outFunction).
Now if we look into outFunction, you will see that it is a function that takes any number of inputs (varargin) and returns the inputFunction evaluated at those arguments. So the returned function behaves exactly like the input function… that’s why it’s called uselessDecorator.
If we were to execute it in the command window, this is how it would look like
decoratedSqrt = uselessDecorator(@sqrt); % decorate the square root function
y = decoratedSqrt(4); % call the decorated function
assert(y == 2)
MATLABThis was, as the name implied, useless. But decorators become really powerful when they actually change the behaviour of the input function. For example, a decorator could print some information on the command window, or modify the inputs being used:
function outFunction = powerDecorator(inFunction)
% This decorator squares the value of the inputs passed into the function
arguments
inFunction (1,1) function_handle
end
outFunction = @(varargin) decorate(inFunction, varargin{:});
end
function varargout = decorate(fun, varargin)
% Square the inputs
ins = cell(1, numel(varargin))
for ii = 1:numel(varargin)
ins{ii} = varargin{ii}^2;
disp("Input #" + ii + " squared: " + varargin{ii} + " -> " + ins{ii});
end
disp("Calling '" + func2str(fun) + "'");
[varargout{1:nargout}] = fun(ins{:});
end
MATLABThe above decorator will take input values of the decorated function and square them before passing them to the original function:
decoratedSqrt = powerDecorator(@sqrt); % decorate the square root function
y = decoratedSqrt(5); % call the decorated function
% >> Input #1 squared: 5 -> 25
% >> Calling 'sqrt'
assert(y == 5)
MATLABYou are probably thinking that you will never need a decorator that takes the inputs to a function and squares them… and you are probably right. But later, in the example section we will show some decorators that can be quite powerful, controlling the execution time of our code, letting us debug it easier, or just adding cool feautures.
How to add decorators into our classes
The decorator signature
Decorators are are actually common in other languages, such as Python, Typescript and (potentially in the near future) Javascript. In general, in these languages the decorator function is added before the method definition they affect:
class APythonClass:
@decorator
def aMethod(self, *args):
...
PythonOf course, in Matlab we will use a different signature. The way I felt more comfortable was by using a custom attribute: “Decorator“, for class methods. Since property setters and getters can also be considered functions, I also defined the “SetDecorator” and “GetDecorator” attributes for class properties:
classdef MyClass < Decoratable
properties (Description = "SetDecorator=@decorator, GetDecorator=@decorator")
...
end
methods (Description = "Decorator = {@decorator1, @decorator2}")
...
end
end
MATLABWhen we use this attributes, we just have to follow them with the function handle of the decorator. If many decorators are used, then they need to be wrapped into a cell array, and they will be invoked from right to left. So
Decorator = {@decorator1, @decorator2}
is equivalent to @decorator1(decorator2(...))
.
The Decoratable class
If a class wants to define decorators, it must be able to parse the Description
properties of its metadata. As we saw in the previous pos, to achieve that we must inherit from another superclass (in the previous example you can see that I named the superclass: Decoratable
).
This part of the blog contains a lot of implementation details. If you are not interested in those details and want to see some example of nice decorators, you can directly skip to the next section. Reading through the limitations might be useful as well.
The constructor
The Decoratable
superclass constructor will be responsible of parsing the metadata, finding the decorator attributes, evaluating the decorator with the method we are decorating, and then storing the decorated function callbacks. We need to keep track of which methods or properties are decorated, so we will store the decorated functions in a map, linking the property or method name to the overloaded function.
classdef (Abstract) Decoratable
properties (Access = private, Hidden)
% Method decorator
Decorators = dictionary(string.empty, function_handle)
% Setter, getter decorators...
end
...
end
MATLABThe constructor needs to:
- Get the metaclass information (as we did on last post) and read the description attribute of properties and method.
- Parse the description string looking for the pieces that start with Decorator, SetDecorator or GetDecorator. Once the decorator functions are found, we can use
eval
to get the actual decorator function handle. - Execute the decorator with our method/getter/setter function. The complicated part here is converting that function into a handle, but we can do it by smartly wrapping the function in a handle that uses
subsref
to call it. - Store the decorated function in our map
function this = Decoratable()
% NOTE: this is only for methods. Property setters and getters
% would have their own logic
% [1]
mc = meta.class.fromName(class(this));
for ii = 1:numel(mc.MethodList)
decoratorString = parseDecorator(mc.MethodList(ii).Description);
if ~isempty(decoratorString)
% [2]
decorator = eval(decoratorString);
% [3]
inFcn = @(varargin) builtin('subsref', this, ...
substruct('.',mc.MethodList(ii).Name,'()',varargin));
outFcn = decorator(inFcn)
% [4]
this.Decorators(mc.MethodList(ii).Name) = outFcn
end
end
end
Overloading the indexing operation
The Decoratable
class also needs to overload the call to a decorated property or method. We can achieve that in 2 metaprogramming ways in Matlab:
- Making the
Decoratable
superclass inherit frommatlab.mixin.indexing.RedefinesDot
, which is a mixin class that lets us define two methods,RedefinesDot.dotReference
andRedefinesDot.dotAsgin
, that control how reference and assignment using the dot operator should behave. Even though this class is *chefs kiss* and simplifies things a lot, it won’t overload property assignment or referencing. If we really want the getter and setter decorators, it will unfortunately not do the trick. - The other possibility is to overload the
subsref
andsubsasgn
methods, which is a feature that has existed long time in Matlab. These methods are really similar to the aforementionedRedefinesDot
class methods, but they are executed whenever an object is indexed with a parenthesis, brace or dot. Obviously, since we are only interested in methods and properties, we will only need to overload the dot indexing (and we make it such that brace and parenthesis calls invoke the builtinsubsasgn
andsubsref
). It will be just a bit more complicated… I have added a snippet of how referencing would be overloaded for our class, with some comments that hopefully let you understand the complexity:
classdef Decoratable
methods (Access = protected)
function varargout = subsref(this, idxOp)
% NOTE: This is only for referencing methods. Property getters and
% setters would have their own logic
% [1] Check if indexed operation is DOT, and if it is a decorated method
if strcmp(idxOp(1), '.') && this.Decorators.isKey(idxOp(1).subs)
% [2] Get decorated method
fcn = this.Decorators(idxOp(1).subs);
% [3] Execute the method
% [3.1] If no other arguments are passed -> y = this.Method
if numel(idxOp) == 1
[varargout{1:nargout}] = fcn();
elseif strcmp(idxOp(2).type, '()')
% [3.2] If other arguments are passed -> y = this.Method(arg1, arg2, ..)
if numel(idxOp) == 2
[varargout{1:nargout}] = fcn(idxOp(2).subs{:});
% [3.3] y = this.Method(arg1, arg2, ..).OutputProperty.(name).(...)
e3se % y = this.Method(u1).something
out = fcn(idxOp(2).subs{:});
out = subsref(out, idxOp(3:end));
end
else % y = this.Method.something
out = fcn();
out = subsref(out, idxOp(2:end));
end
else % '()' or '{}' or undecorated methods
[varargout{1:nargout}] = builtin('subsref',this,idxOp);
end
end
...
end
end
MATLABLimitations
As you may imagine, the way in which the Decoratable
class is defined imposes some limitations in when the decorators will be called, and how will they be called:
- The decorated methods will only be called if they are dot-indexed from an object. So
object.method()
will invoke the decorated method, butmethod(object)
won’t. This is because we are overloading the indexing methods of the objects, but not the methods themselves. I don’t think this is a big deal though, since I guess most of the people use dot indexing for class methods and leave parenthesis to functions. - The decorated methods are only called when the method is invoked outside the subclass. If another object (or the user) invokes the method, it will be decorated. If a private method calls the decorated one inside it, the builtin method will be executed and not the decorated one.
- The decorated functions must have the same amount of inputs and outputs as the undecorated ones. This implies that:
- property getters should have one input (the object), and one output (the property)
- setters should have two inputs (the object and the value to be set), and one output (the object again). If the object is a handle, then setters can have no outputs
- decorated methods should have the same amount of output arguments. On the inputs there’s no constraint though.
This is a bit more tricky to explain, but it has to do with how many output arguments Matlab expects after an expression or assignment. While it is clear that a = object.method()
returns one output, [a{:}] = output.method();
may return more, and subsref
needs to know that. By default Matlab uses the builtin numArgumentsForSubscript
function to determine the number of elements expected (to be honest, I don’t know how it works internally). Overloading the method to remove our limitation is possible, but in my experience this is complicated and can lead to unwanted side-effects. Enforcing that the decorated functions have the same signatures removed most of the difficulties I faced while designing the class.
- Finally, our decorator functions are extracted from the description attribute by means of the
eval
function. That implies that the decorator function must be accessible by theDecoratable
superclass, which means that we can’t define decorators as sub-functions of our subclasses. Instead, we will have to define them in their own files.
Some useful decorators
Are decorators useful? Well, that depends on you. If you ask around Python forums, you’ll see that some people actually love them. But if you go to a Typescript/Javascript one… well, the consensus is that they shouldn’t exist.
The point that decorator “haters” make is pretty reasonable: the logic that you put inside the decorator could go inside the getter, setter or method; and decorators will also obfuscate the code (instead of only looking into the function, you also need to check what the decorator does to understand the function). On the other side, “lovers” will argue that decorators can avoid code duplication, simplify the functions and, if properly named, their behaviour will be clear immediately.
I want to show a couple of decorators that I have found useful in the past, and you can make up your mind on whether you love them or hate them.
Caching
Memoizing or caching is the process in which we store the output of a function and using the next times the function is called with the same arguments. This is particularly useful when we call a function many times and, by that logic, recursion. Take for example the Fibonacci function, in which a value is computed as the sum of its two previous ones:
function f = fibonacci(n)
if n == 0
f = 0;
elseif n == 1
f = 1;
else
f = fibonacci(n-1) + fibonacci(n-2);
end
end
Now let’s say that we want to compute the values of the Fibonacci at a very high number. The function will be quite slow, and that’s because we are calling recursively the function, over and over. For example, for n = 1000
, the function will require computing itself at n = 999
and at n = 998
. At n = 999
, it will have to compute n = 998
and n = 997
, and so on. This complexity of this function grows exponentially, which is pretty bad.
But you might have noticed that for n = 999
we already compute n = 998
, so there’s actually no need it call it twice if we save the value in memory. And that’s exactly what caching is: improving performance by storing in memory.
Matlab already has a memoize
function that does exactly this (and it is implemented in a very clever way using RedefinesDot
) for functions. But for classes there’s nothing similar. A cache method decorator could look like this:
function outFunction = cacheDecorator(inFunction)
cache = struct.empty();
outFunction = @(varargin) decorated(inFunction, varargin{:});
function varargout = decorated(fun, varargin)
% [1] check if the inputs have been used (cache)
for ii = numel(cache):-1:1
% [2] if equal, return the stored output
if isequaln(cache(ii).Inputs, varargin)
varargout = cache(ii).Outputs;
end
end
% [3] Nothing in the cache matched, so call the original function
[varargout{1:nargout}] = fun(varargin{:});
% [4] And store the results in the cache!
cache(end + 1) = struct("Inputs", varargin, "Outputs", varargout);
end
end
This decorator uses nested functions to keep the cache variable after the decorated function returns (it’s not global nor persistent!). Just by adding a simple Decorator = @cacheDecorator
, we can have a bunch of class methods that use memoization.
Note 1: Think twice before applying memoization. If the methods have side effects or depends in non constant properties of the class, it is probably not a good idea to cache the calls!
Note 2: The @cacheDecorator
is just a simple approach to memoization and in no way it is complete. A good memoization decorator would have a maximum cache size (you know, we don’t want infinitely big caches), and a better way to organize the cache to optimize the search (maybe the cached entry that’s been used more times is the first one, or the last one).
Timed execution
I use a lot AppDesigner, and sometimes I have faced a problem that you might also be familiar with: I have a button that, when pressed, executes a big, time consuming function. My users, impatient as they are, just click 50 times the button because they think it will be… faster? Instead, what they are doing is calling the big function 50 times, which will just take forever. To avoid the problem, one can just set the BusyAction
property to ‘cancel’. That will avoid that, while the function is running, it is re-called.
But sometimes you don’t want to cancel future calls, only limit them. This is what debounce and throttle, our next decorators, are for:
- Debouncing a function is like saying: call the function as many times as you want, it will only execute when you stop for at least X milliseconds.
- Throttling is similar, but saying: call the function as fast as many times as you want, it will only execute at most every X milliseconds.
As an example, think of how the debounce and throttle methods would affect the ValueChangingFcn
of a slider.
- Without anything, the function triggers every time the slider moves just a tiny bit.
- Debouncing the callback with 100ms, it will trigger once only after the user stops moving the slider for 100ms.
- Throttling it at 100ms will cause it to execute only every 100ms that the slider is moving. So if the user takes 1 second to move the slider from one side to the other, the callback will be executed 10 times.
These decorators can be implemented by using single-shot timer objects. Here’s how a debounce and throttle decorator could look like in Matlab:
function outFunction = debounceDecorator(inFunction)
delay = 1; % 1 second delay
t = timer("StartDelay", delay, "ExecutionMode", "singleShot");
outFunction = @(varargin) decorateMethod(inFunction, varargin{:});
function decorateMethod(fn, varargin)
% Delete timers of previous calls -> they wont execute the method
t.stop();delete(t);
% Create a new timer that, after 1s (if not deleted), executes the function
t = timer("StartDelay", delay, "ExecutionMode", "singleShot");
t.TimerFcn = @(~,~) fn(src, varargin{:});
t.start();
end
end
function outFunction = throttleDecorator(inFunction)
delay = 1; % 1 second delay
t = timer("StartDelay", delay, "ExecutionMode", "singleShot");
t.TimerFcn = @(~,~) [];
outFunction = @(varargin) decorateMethod(inFunction, varargin{:});
function decorateMethod(fn, varargin)
% Only execute if the timer is NOT running
if strcmp(t.Running, "off")
fn(varargin{:});
t.start(); % restart the timer for another 1 second
end
end
end
Note: These decorators are useful only for functions that don’t have outputs. That’s why callbacks are excellent candidates.
Conclusions
I actually see the point in decorators. When used correctly they can be a great way to simplify and improve our classes. But, of course, they can be overused and misused, specially if one doesn’t know what they are actually doing.
Anyways, I’d love to see something like this being implemented in Matlab (and not having to use the description hack). Mathworks has added recently a bunch of changes to improve functional and object-oriented programming (the argument block for inputs and outputs is fantastic). So who knows? Maybe custom attributes and decorators are features that arrive at some point.