Hauptinhalt

Ergebnisse für

John
John
Letzte Aktivität vor etwa 14 Stunden

The "Issues" with Constant Properties
MATLABs Constant properties can be rather difficult to deal with at times. For those unfamiliar there are two distinct behaviors when accessing constant properties of a class. If a "static" pattern is used ClassName.PropName the the value, as it was assigned to the property is returned; that is to say that you will have a nargout of 1. But, rather frustratingly, if an instance of the class is used when accessing the constant property, such as ArrayOfClassName.PropName then your nargout will be equivalent to the number of elements in the array; this means that functionally the constant property accessing scheme is identical to that of the element wise properties you find on "instance" properties.
Motivation for Correcting Constant Property Behavior
This can be frustraing since constant properties are conceptually designed to tie data to a class. You could see this design pattern being useful where a super class were to define an abstract constant property, that would drive the behavior of subclasses; the subclasses define the value of the property and the super class uses it. I would like to use this design to develop a custom display "MatrixDisplay" focused mixin (like the internal matlab.mixin.internal.MatrixDisplay). The idea is that missing element labels, invalid handle element labels, and other semantic values can be configured by the subclasses, conveniently just by setting the constant properties; these properties will be used by the super class to substitute the display strings of appropriate elements. Most of the processing will happen within the super class as to enable simple, low-investment, opt-in display options for array style classes.
The issue is that you can not rely on constant property access to return the appropriate value when the instance you've been passed is empty. This also happens with excessive outputs when the instance is non-scalar, but those extra values from the CSL are just ignored, while Id imagine there is an effect on performance from generating the excess outputs (assuming theres no internal optimization for the unused outputs), this case still functions appropriately. As I enjoying exploring MATLAB, I found an internal indexing mixing class in the past that provides far greater control of do indexing; I've done a good deal of neat things with it, though at the cost of great overhead when getting implementing cool "proof of concept/showcase" examples. Today I used it to quickly implement a mix in that "fixes" constant properties such that they always return as though they were called statically from the class name, as opposed to an instance.
A Simplistic Solution
To do this I just intercepted the property indexing, checked if it was constant, and used the DefaultValue property of the metadata to return the value. This works nicely since we are required to attempt to initialize a "dummy" scalar array, or generate a function handle; both of those would likely be slower, and in the former case, may not be possible depending on the subclass implementation. It is worth noting that this method of querying the value from metadata is safe because constant properties are immutable and thus must be established as the class is loaded. Below is the small utility class I have implemented to get predictable constant variable access into classes that benefit from it. Lastly it is worth noting that I've not torture testing the rerouting of the indexing we aren't intercepting, in my limited play its behaved as expected but it may be worth looking over if you end up playing around with this and notice abnormal property assignment or reading from non-constant properties.
Sample Class Implementation
classdef(Abstract, HandleCompatible) ConstantProperty < matlab.mixin.internal.indexing.RedefinesDotProperties
%ConstantProperty Returns instance indexed constant properties as though they were statically indexed.
% This class overloads property access to check if the indexed property is constant and return it properly.
%% Property membership utility methods
methods(Access=private)
function [isConst, isProp] = isConstantProp(obj, prop, options)
%isConstantProp Determine if the input property names are constant properties of the input
arguments
obj mixin.ConstantProperty;
prop string;
options.Flatten (1, 1) logical = false;
end
% Store the cache to avoid rechecking string membership and parsing metadata
persistent class_cache
% Initialize cache for all subclasses to maintain their own caches
if(isempty(class_cache))
class_cache = configureDictionary("string", "dictionary");
end
% Gather the current class being analyzed
classname = string(class(obj));
% Check if the current class has a cache, if not make one
if(~isKey(class_cache, classname))
class_cache(classname) = configureDictionary("string", "struct");
end
% Alias the current classes cache
prop_cache = class_cache(classname);
% Check which inputs are already cached
isCached = isKey(prop_cache, prop);
% Add any values that have yet to be cached to the cache
if(any(~isCached, "all"))
% Flatten cache additions
props = row(prop(~isCached));
% Gather the meta-property data of the input object and determine if inputs are listed properties
mc_props = metaclass(obj).PropertyList;
% Determine which properties are keys
[isConst, idx] = ismember(props, string({mc_props.Name}));
idx = idx(isConst);
% Check which of the inputs are constant properties
isConst = repmat(isConst, 2, 1);
isConst(1, isConst(1, :)) = [mc_props(idx).Constant];
% Parse the results into structs for caching
cache_values = cell2struct(num2cell(isConst), ["isConst"; "isProp"]);
prop_cache(props) = row(cache_values);
% Re-sync the cache
class_cache(classname) = prop_cache;
end
% Extract results from the cache
values = prop_cache(prop);
if(options.Flatten)
% Split and reshape output data
sz = size(prop);
isConst = reshape(values.isConst, sz);
isProp = reshape(values.isProp, sz);
else
isConst = struct2cell(values);
end
end
function [isConst, isProp] = isConstantIdxOp(obj, idxOp)
%isConstantIdxOp Determines if the idxOp is referencing a constant property.
arguments
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
import matlab.indexing.IndexingOperationType;
if(idxOp(1).Type == IndexingOperationType.Dot)
[isConst, isProp] = isConstantProp(obj, idxOp(1).Name);
else
[isConst, isProp] = deal(false);
end
end
function A = getConstantProperty(obj, idxOp)
%getConstantProperty Returns the value of a constant property using a static reference pattern.
arguments
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
A = findobj(metaclass(obj).PropertyList, "Name", idxOp(1).Name).DefaultValue;
end
end
%% Dot indexing methods
methods(Access = protected)
function A = dotReference(obj, idxOp)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
arguments(Output, Repeating)
A
end
% Force at least one output
N = max(1, nargout);
% Check if the indexing operation is a property, and if that property is constant
[isConst, isProp] = isConstantIdxOp(obj, idxOp);
if(~isProp)
% Error on invalid properties
throw(MException( ...
"JB:mixin:ConstantProperty:UnrecognizedProperty", ...
"Unrecognized property '%s'.", ...
idxOp(1).Name ...
));
elseif(isConst)
% Handle forwarding indexing operations
if(isscalar(idxOp))
% Direct assignment
[A{1:N}] = getConstantProperty(obj, idxOp);
else
% First extract constant property then forward indexing operations
tmp = getConstantProperty(obj, idxOp);
[A{1:N}] = tmp.(idxOp(2:end));
end
else
% Handle forwarding indexing operations
if(isscalar(idxOp))
% Unfortunately we can't just recall obj.(idxOp) to use default/built-in so we manually extract
[A{1:N}] = obj.(idxOp.Name);
else
% Otherwise let built-in handling proceed
tmp = obj.(idxOp(1).Name);
[A{1:N}] = tmp.(idxOp(2:end));
end
end
end
function obj = dotAssign(obj, idxOp, values)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
end
arguments(Input, Repeating)
values
end
% Handle assignment based on presence of forward indexing
if(isscalar(idxOp))
% Simple broadcasted assignment
[obj.(idxOp.Name)] = deal(values{:});
else
% Initialize the intermediate values and expand the values for assignment
tmp = {obj.(idxOp(1).Name)};
[tmp.(idxOp(2:end))] = deal(values{:});
% Reassign the modified data to the output object
[obj.(idxOp(1).Name)] = deal(tmp{:});
end
end
function n = dotListLength(obj, idxOp, idxCnt)
arguments(Input)
obj mixin.ConstantProperty;
idxOp (1, :) matlab.indexing.IndexingOperation;
idxCnt (1, :) matlab.indexing.IndexingContext;
end
if(isConstantIdxOp(obj, idxOp))
if(isscalar(idxOp))
% Constant properties will also be 1
n = 1;
else
% Checking forwarded indexing operations on the scalar constant property
n = listLength(obj.(idxOp(1).Name), idxOp(2:end), idxCnt);
end
else
% Check the indexing operation normally
% n = listLength(obj, idxOp, idxCnt);
n = numel(obj);
end
end
end
end
You may have come across code that looks like that in some languages:
stubFor(get(urlPathEqualTo("/quotes"))
.withHeader("Accept", equalTo("application/json"))
.withQueryParam("s", equalTo(monitoredStock))
.willReturn(aResponse())
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\\"symbol\\": \\"XYZ\\", \\"bid\\": 20.2, " + "\\"ask\\": 20.6}")))
That’s Java. Even if you can’t fully decipher it, you can get a rough idea of what it is supposed to do, build a rather complex API query.
Or you may be familiar with the following similar and frequent syntax in Python:
import seaborn as sns
sns.load_dataset('tips').sample(10, random_state=42).groupby('day').mean()
Here’s is how it works: multiple method calls are linked together in a single statement, spanning over one or several lines, usually because each method returns the same object or another object that supports further calls.
That technique is called method chaining and is popular in Object-Oriented Programming.
A few years ago, I looked for a way to write code like that in MATLAB too. And the answer is that it can be done in MATLAB as well, whevener you write your own class!
Implementing a method that can be chained is simply a matter of writing a method that returns the object itself.
In this article, I would like to show how to do it and what we can gain from such a syntax.
Example
A few years ago, I first sought how to implement that technique for a simulation launcher that had lots of parameters (far too many):
lauchSimulation(2014:2020, true, 'template', 'TmplProd', 'Priority', '+1', 'Memory', '+6000')
As you can see, that function takes 2 required inputs, and 3 named parameters (whose names aren’t even consistent, with ‘Priority’ and ‘Memory’ starting with an uppercase letter when ‘template’ doesn’t).
(The original function had many more parameters that I omit for the sake of brevity. You may also know of such functions in your own code that take a dozen parameters which you can remember the exact order.)
I thought it would be nice to replace that with:
SimulationLauncher() ...
.onYears(2014:2020) ...
.onDistributedCluster() ... % = equivalent of the previous "true"
.withTemplate('TmplProd') ...
.withPriority('+1') ...
.withReservedMemory('+6000') ...
.launch();
The first 6 lines create an object of class SimulationLauncher, calls several methods on that object to set the parameters, and lastly the method launch() is called, when all desired parameters have been set.
To make it cleared, the syntax previously shown could also be rewritten as:
launcher = SimulationLauncher();
launcher = launcher.onYears(2014:2020);
launcher = launcher.onDistributedCluster();
launcher = launcher.withTemplate('TmplProd');
launcher = launcher.withPriority('+1');
launcher = launcher.withReservedMemory('+6000');
launcher.launch();
Before we dive into how to implement that code, let’s examine the advantages and drawbacks of that syntax.

Benefits and drawbacks

Because I have extended the chained methods over several lines, it makes it easier to comment out or uncomment any one desired option, should the need arise. Furthermore, we need not bother any more with the order in which we set the parameters, whereas the usual syntax required that we memorize or check the documentation carefully for the order of the inputs.
More generally, chaining methods has the following benefits and a few drawbacks:
Benefits:
  • Conciseness: Code becomes shorter and easier to write, by reducing visual noise compared to repeating the object name.
  • Readability: Chained methods create a fluent, human-readable structure that makes intent clear.
  • Reduced Temporary Variables: There's no need to create intermediary variables, as the methods directly operate on the object.
Drawbacks:
  • Debugging Difficulty: If one method in a chain fails, it can be harder to isolate the issue. It effectively prevents setting breakpoints, inspecting intermediate values, and identifying which method failed.
  • Readability Issues: Overly long and dense method chains can become hard to follow, reducing clarity.
  • Side Effects: Methods that modify objects in place can lead to unintended side effects when used in long chains.

Implementation

In the SimulationLauncher class, the method lauch performs the main operation, while the other methods just serve as parameter setters. They take the object as input and return the object itself, after modifying it, so that other methods can be chained.
classdef SimulationLauncher
properties (GetAccess = private, SetAccess = private)
years_
isDistributed_ = false;
template_ = 'TestTemplate';
priority_ = '+2';
memory_ = '+5000';
end
methods
function varargout = launch(obj)
% perform whatever needs to be launched
% using the values of the properties stored in the object:
% obj.years_
% obj.template_
% etc.
end
function obj = onYears(obj, years)
assert(isnumeric(years))
obj.years_ = years;
end
function obj = onDistributedCluster(obj)
obj.isDistributed_ = true;
end
function obj = withTemplate(obj, template)
obj.template_ = template;
end
function obj = withPriority(obj, priority)
obj.priority_ = priority;
end
function obj = withMemory( obj, memory)
obj.memory_ = memory;
end
end
end
As you can see, each method can be in charge of verifying the correctness of its input, independantly. And what they do is just store the value of parameter inside the object. The class can define default values in the properties block.
You can configure different launchers from the same initial object, such as:
launcher = SimulationLauncher();
launcher = launcher.onYears(2014:2020);
launcher1 = launcher ...
.onDistributedCluster() ...
.withReservedMemory('+6000');
launcher2 = launcher ...
.withTemplate('TmplProd') ...
.withPriority('+1') ...
.withReservedMemory('+7000');
If you call the same method several times, only the last recorded value of the parameter will be taken into acount:
launcher = SimulationLauncher();
launcher = launcher ...
.withReservedMemory('+6000') ...
.onDistributedCluster() ...
.onYears(2014:2020) ...
.withReservedMemory('+7000') ...
.withReservedMemory('+8000');
% The value of "memory" will be '+8000'.
If the logic is still not clear to you, I advise you play a bit with the debugger to better understand what’s going on!

Conclusion

I love how the method chaining technique hides the minute detail that we don’t want to bother with when trying to understand what a piece of code does.
I hope this simple example has shown you how to apply it to write and organise your code in a more readable and convenient way.
Let me know if you have other questions, comments or suggestions. I may post other examples of that technique for other useful uses that I encountered in my experience.
This is a feature which doesn't apear to currently exist, but I think alot of matlab users would like, particularly ones who write alot of custom classes.
Imagine i have a custom class with some properties:
classdef CustomClass < handle
properties
name (1,1) string = "default name"
varOne (1,1) double = 0
end
methods
function obj = CustomClass(name,varOne)
obj.name = name;
obj.VarOne = varOne;
end
end
end
Then imagine I have a function which returns one of these custom class objects:
function [obj] = Calculation(Var1,Var2,name)
arguments (Input)
Var1 (1,1) double
Var2 (1,1) double
end
arguments (Output)
obj (1,1) CustomClass
end
results = Var1 + Var2;
obj = CustomClass(name,result);
end
With this class and this function which returns one of these class objects, I would like the fact that I provided "(1,1) CustomClass" in the output arguemnts block of function "Calculation(Var1,Var2,name)" to trigger code assist automaticaly show me, when writing code that the retuned value from this funciton has properties "name" and "varOne" in the object.
For istance, if I write the following code with this function and the class in the Matlab search path
testObj = Calculation(1,1,"test");
testObj.varOne = 10; %the property "varOne" doesn't apear in code assist when writing this line of code
I would like that the fact function "Calcuation(Var1,Var2,name) has the output arguments block enforcing that this function must return an object of "CustomClass" to make code assist recognise that "testObj" is a "CustomClass" object, just as if testObj was an input argument to another function which had an input argument requiring that "testObj" was a "CustomClass" object.
Maybe this is a feature that may be added to matlab in future releases? (please and thank you LOL)