Search code examples
matlaboopdynamic-properties

Dynamically assign the getter for a dependent property in MATLAB


In Matlab, I can define a class as such:

classdef klass < handle
    properties(Dependent)
        prop
    end
end

Matlab is perfectly happy instantiating an object of this class, even without defining a getter for prop. It only fails when I try to access it (understandably). I'd like to set the GetMethod dynamically based upon the property's name.

Unfortunately, even when the property is Dependent, the meta.property field for GetMethod is still read-only. And while inheriting from dynamicprops could allow adding a property and programmatically setting its GetMethod in every instance, I don't believe it could be used to change an existing property. I may have to go this route, but as prop must exist for every object I'd prefer to simply set the getter on a class-by-class basis. Is such a thing possible?

An alternative solution could be through some sort of catch-all method. In other languages, this could be accomplished through a Ruby-like method_missing or a PHP-like __get(). But as far as I know there's no (documented or otherwise) analog in Matlab.


(My use case: this class gets inherited by many user-defined subclasses, and all their dependent properties are accessed in a similar way, only changing based on the property name. Instead of asking users to write get.* methods wrapping a call to the common code for each and every one of their dependent properties, I'd like to set them all dynamically with anonymous function pointers containing the necessary metadata).


Solution

  • Here is my proposal: create a method in the superclass called add_dyn_prop. This method is to be called in the subclasses instead of creating a dependent property the usual way.

    The idea is that the superclass inherit from dynamicprops and use addprop to add a new property, and set its accessor methods manually based on its name.

    classdef klass < dynamicprops
        methods (Access = protected)
            function add_dyn_prop(obj, prop, init_val, isReadOnly)
                % input arguments
                narginchk(2,4);
                if nargin < 3, init_val = []; end
                if nargin < 4, isReadOnly = true; end
    
                % create dynamic property
                p = addprop(obj, prop);
    
                % set initial value if present
                obj.(prop) = init_val;
    
                % define property accessor methods
                % NOTE: this has to be a simple function_handle (@fun), not
                % an anonymous function (@()..) to avoid infinite recursion
                p.GetMethod = @get_method;
                p.SetMethod = @set_method;
    
                % nested getter/setter functions with closure
                function set_method(obj, val)
                    if isReadOnly
                        ME = MException('MATLAB:class:SetProhibited', sprintf(...
                          'You cannot set the read-only property ''%s'' of %s', ...
                          prop, class(obj)));
                        throwAsCaller(ME);
                    end
                    obj.(prop) = val;
                end
                function val = get_method(obj)
                    val = obj.(prop);
                end
            end
        end
    end
    

    now in the subclass, instead of defining a dependent property the usual way, we use this new inherited function in the constructor to define a dynamic property:

    classdef subklass < klass
        %properties (Dependent, SetAccess = private)
        %    name
        %end
        %methods
        %    function val = get.name(obj)
        %        val = 'Amro';
        %    end
        %end
    
        methods
            function obj = subklass()
                % call superclass constructor
                obj = obj@klass();
    
                % define new properties
                add_dyn_prop(obj, 'name', 'Amro');
                add_dyn_prop(obj, 'age', [], false)
            end            
        end
    end
    

    The output:

    >> o = subklass
    o = 
      subklass with properties:
    
         age: []
        name: 'Amro'
    >> o.age = 10
    o = 
      subklass with properties:
    
         age: 10
        name: 'Amro'
    >> o.name = 'xxx'
    You cannot set the read-only property 'name' of subklass. 
    

    Of course now you can customize the getter method based on the property name as you initially intended.


    EDIT:

    Based on the comments, please find below a slight variation of the same technique discussed above.

    The idea is to require the subclass to create a property (defined as abstract in the superclass) containing the names of the desired dynamic properties to be created. The constructor of the superclass would then create the specified dynamic properties, setting their accessor methods to generic functions (which could customize their behavior based on the property name as you requested). I am reusing the same add_dyn_prop function I mentioned before.

    In the subclass, we are simply required to implement the inherited abstract dynamic_props property, initialized with a list of names (or {} if you dont want to create any dynamic property). For example we write:

    classdef subklass < klass
        properties (Access = protected)
            dynamic_props = {'name', 'age'}
        end
    
        methods
            function obj = subklass()
                obj = obj@klass();
            end
        end
    end
    

    The superclass is similar to what we had before before, only now is it its responsibility to call the add_dyn_prop in its constructor for each of the property names:

    classdef klass < dynamicprops        % ConstructOnLoad
        properties (Abstract, Access = protected)
            dynamic_props
        end
        methods
            function obj = klass()
                assert(iscellstr(obj.dynamic_props), ...
                    '"dynamic_props" must be a cell array of strings.');
                for i=1:numel(obj.dynamic_props)
                    obj.add_dyn_prop(obj.dynamic_props{i}, [], false);
                end
            end
        end
    
        methods (Access = private)
            function add_dyn_prop(obj, prop, init_val, isReadOnly)
                % input arguments
                narginchk(2,4);
                if nargin < 3, init_val = []; end
                if nargin < 4, isReadOnly = true; end
    
                % create dynamic property
                p = addprop(obj, prop);
                %p.Transient = true;
    
                % set initial value if present
                obj.(prop) = init_val;
    
                % define property accessor methods
                p.GetMethod = @get_method;
                p.SetMethod = @set_method;
    
                % nested getter/setter functions with closure
                function set_method(obj,val)
                    if isReadOnly
                        ME = MException('MATLAB:class:SetProhibited', sprintf(...
                          'You cannot set the read-only property ''%s'' of %s', ...
                          prop, class(obj)));
                        throwAsCaller(ME);
                    end
                    obj.(prop) = val;
                end
                function val = get_method(obj)
                    val = obj.(prop);
                end
            end
        end
    end
    

    Note: I did not use ConstructOnLoad class attribute or Transient property attribute, as I am still not sure how they would affect loading the object from a saved MAT-file in regards to dynamic properties.

    >> o = subklass
    o = 
      subklass with properties:
    
         age: []
        name: []
    
    >> o.name = 'Amro'; o.age = 99
    o = 
      subklass with properties:
    
         age: 99
        name: 'Amro'