Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

matlab subsref: {} with string argument fails, why?

There are a few implementations of a hash or dictionary class in the Mathworks File Exchange repository. All that I have looked at use parentheses overloading for key referencing, e.g.

d = Dict;
d('foo') = 'bar';
y = d('foo');

which seems a reasonable interface. It would be preferable, though, if you want to easily have dictionaries which contain other dictionaries, to use braces {} instead of parentheses, as this allows you to get around MATLAB's (arbitrary, it seems) syntax limitation that multiple parentheses are not allowed but multiple braces are allowed, i.e.

t{1}{2}{3}  % is legal MATLAB
t(1)(2)(3)  % is not legal MATLAB

So if you want to easily be able to nest dictionaries within dictionaries,

dict{'key1'}{'key2'}{'key3'}

as is a common idiom in Perl and is possible and frequently useful in other languages including Python, then unless you want to use n-1 intermediate variables to extract a dictionary entry n layers deep, this seems a good choice. And it would seem easy to rewrite the class's subsref and subsasgn operations to do the same thing for {} as they previously did for (), and everything should work.

Except it doesn't when I try it.

Here's my code. (I've reduced it to a minimal case. No actual dictionary is implemented here, each object has one key and one value, but this is enough to demonstrate the problem.)

classdef TestBraces < handle

    properties
        % not a full hash table implementation, obviously
        key
        value
    end

    methods(Access = public)


        function val = subsref(obj, ref)
            % Re-implement dot referencing for methods.
            if strcmp(ref(1).type, '.')
                % User trying to access a method             
                % Methods access
                if ismember(ref(1).subs, methods(obj))
                    if length(ref) > 1
                        % Call with args
                        val = obj.(ref(1).subs)(ref(2).subs{:});
                    else
                        % No args
                        val = obj.(ref.subs);
                    end
                    return;
                end                
                % User trying to access something else.
                error(['Reference to non-existant property or method ''' ref.subs '''']);
            end
            switch ref.type
                case '()'
                    error('() indexing not supported.');
                case '{}'
                    theKey = ref.subs{1};
                    if isequal(obj.key, theKey)
                        val = obj.value;
                    else
                        error('key %s not found', theKey);
                    end
                otherwise
                    error('Should never happen')
            end
        end    

        function obj = subsasgn(obj, ref, value)
            %Dict/SUBSASGN  Subscript assignment for Dict objects.
            %
            %  See also: Dict
            %

            if ~strcmp(ref.type,'{}')
                error('() and dot indexing for assignment not supported.');
            end

            % Vectorized calls not supported
            if length(ref.subs) > 1
                error('Dict only supports storing key/value pairs one at a time.');
            end
            theKey = ref.subs{1};
            obj.key = theKey;
            obj.value = value;
        end % subsasgn        
    end     
end

Using this code, I can assign as expected:

t = TestBraces;
t{'foo'} = 'bar'

(And it is clear that the assignment work from the default display output for t.) So subsasgn appears to work correctly.

But I can't retrieve the value (subsref doesn't work):

t{'foo'}
??? Error using ==> subsref
Too many output arguments.

The error message makes no sense to me, and a breakpoint at the first executable line of my subsref handler is never hit, so at least superficially this looks like a MATLAB issue, not a bug in my code.

Clearly string arguments to () parenthesis subscripts are allowed, since this works fine if you change the code to work with () instead of {}. (Except then you can't nest subscript operations, which is the object of the exercise.)

Either insight into what I'm doing wrong in my code, any limitations that make what I'm doing unfeasible, or alternative implementations of nested dictionaries would be appreciated.

like image 979
jmhl Avatar asked Jan 03 '12 14:01

jmhl


1 Answers

Short answer, add this method to your class:

function n = numel(obj, varargin)
    n = 1;
end

EDIT: The long answer.

Despite the way that subsref's function signature appears in the documentation, it's actually a varargout function - it can produce a variable number of output arguments. Both brace and dot indexing can produce multiple outputs, as shown here:

>> c = {1,2,3,4,5};
>> [a,b,c] = c{[1 3 5]}
a =
     1
b =
     3
c =
     5

The number of outputs expected from subsref is determined based on the size of the indexing array. In this case, the indexing array is size 3, so there's three outputs.

Now, look again at:

t{'foo'}

What's the size of the indexing array? Also 3. MATLAB doesn't care that you intend to interpret this as a string instead of an array. It just sees that the input is size 3 and your subsref can only output 1 thing at a time. So, the arguments mismatch. Fortunately, we can correct things by changing the way that MATLAB determines how many outputs are expected by overloading numel. Quoted from the doc link:

It is important to note the significance of numel with regards to the overloaded subsref and subsasgn functions. In the case of the overloaded subsref function for brace and dot indexing (as described in the last paragraph), numel is used to compute the number of expected outputs (nargout) returned from subsref. For the overloaded subsasgn function, numel is used to compute the number of expected inputs (nargin) to be assigned using subsasgn. The nargin value for the overloaded subsasgn function is the value returned by numel plus 2 (one for the variable being assigned to, and one for the structure array of subscripts).

As a class designer, you must ensure that the value of n returned by the built-in numel function is consistent with the class design for that object. If n is different from either the nargout for the overloaded subsref function or the nargin for the overloaded subsasgn function, then you need to overload numel to return a value of n that is consistent with the class' subsref and subsasgn functions. Otherwise, MATLAB produces errors when calling these functions.

And there you have it.

like image 89
kwatford Avatar answered Nov 16 '22 08:11

kwatford