bugGNU Octave - Bugs: bug #56856, Matlab calls class subsasgn method...

 
 

bug #56856: Matlab calls class subsasgn method with array of double instead of class type

Submitted by:  None
Submitted on:  Wed 04 Sep 2019 08:11:10 AM UTC  
 
Category:  Interpreter Severity:  3 - Normal
Priority:  3 - Low Item Group:  WTF, Matlab?!?
Status:  Works For Me Assigned to:  None
Originator Name:  JD Originator Email:  -email is unavailable-
Open/Closed:  Open Release:  5.1.0
Operating System:  GNU/Linux

Add a New Comment (Rich Markup)
   

You are not logged in

Please log in, so followups can be emailed to you.

 

( Jump to the original submission)

Tue 10 Sep 2019 05:17:45 PM UTC, comment #9: 

Based on all the questions and issues that have been raised here by you and me, I think it's not exactly clear what the right behavior should be. I don't plan on looking into this much more at the moment, but leaving this open for others to do further investigation if they see this as a problem.

Mike Miller <mtmiller>
Project Administrator
Mon 09 Sep 2019 10:11:43 PM UTC, comment #8: 

comment #6:

> I would also like to point out that if the LHS value is passed as a double array, then a simple override like this that currently works in Octave would start to fail:
>

> function y = subsasgn (a, s, b)
>   a(s.subs{:}) = b;
>   y = a;
> endfunction

In which context are you using this function? Why isn't it creating an infinite loop?

> I'm also confused why the 'val' value is passed in as a double array, but a direct call to subsasgn like this with a builtin class fails in Matlab:
>

> y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())
>
> The following error occurred converting from inputParser to double:
> Conversion to double from inputParser is not possible.

Sorry, I'm not familiar with inputParser... not sure what you expect to see here.

Anonymous
Mon 09 Sep 2019 10:07:01 PM UTC, comment #7: 

Thanks for looking into this issue!

I agree that this behavior appears in a very specific situation. I have however seen quite often people use a loop to grow a variable, as in

for i = 1:10
    a(i) = f(i);
end

This is of course not the best code, but it is also very easy and natural for small loops, so people just use it (and I don't have control over that). In fact it is also supported in Octave (e.g. set 'f = @(x) x;'). When the function f produces a class object, this directly leads to the current issue.

> It seems prudent to me to avoid relying on the type or dimensions of the left hand side value, other than to do the assignment that the subsasgn call is providing.

I'm not sure exactly what you mean here: isn't it crucial to be able to evaluate the type and size of the left-hand-side of an assignment in order to perform this assignment correctly? For instance, if 'x(10)=2' is called on an object x which has only 9 elements, then the size of this object first needs to be increased before the value of '2' can be assigned to its 10th element, right?

From the point of view of a class developer, I see one very big reason why the current octave behavior is problematic. It is seen when running the same two commands below in octave:

octave:1> a=test;
calling the test constructor
octave:2> x(10)=a
Calling test.subsasgn(val, idx, rhs).
val is of type 'test':
val =

<object array test>

idx is of type 'struct':
idx =

  scalar structure containing the fields:

    type = ()
    subs =
    {
      [1,1] =  10
    }

rhs is of type 'test':
rhs =

<object test>

Now we check the size of 'val':
Calling test.size.
warning: size: some elements in list of return values are undefined
warning: called from
    size at line 4 column 1
    subsasgn at line 11 column 5
[](0x0)
warning: subsasgn: some elements in list of return values are undefined
warning: called from
    subsasgn at line 12 column 1
x = [](0x0)

Ok, this produces an error, but the real issue is at the beginning: variable 'val' is provided to the function 'subsasgn' without being constructed... it is of type 'test', but the test class constructor has not been called: an object was created without calling the class constructor. Isn't this in contradiction with the very basis of object oriented programming? What is this if not a source of unspecified behaviors? Honestly, if an object oriented language requires you to deal with un-initialized objects, then either it is actually not an object oriented language, or there is a problem with the language!

One way to avoid this could be to call the class constructor with an empty input since we know nothing about 'x', e.g. an empty array. But this would require that every class constructor should implement a default constructor, which I don't think is currently a requirement.

Another solution would be not to interpret the type of undeclared variables further than necessary (an empty array then seem like a reasonable common ground for all situations). It looks to me that this is the solution chosen by matlab, and I think it has the merit of being free of presuppositions: why should 'x(10) = a' produce by default an 'x' which is of the same type as 'a'? If 'a' represents the image of a function, then 'x' should represent the function which sends 10 to a, and is therefore of a different type. But if an empty 'x' is interpreted as being of the same class as 'a' even though the user did not specify so, then this reduced the set of possibilities.

Also, after all, subsasgn is just another function. So if an empty argument can be interpreted in a context-dependent way for this function, why shouldn't it be like that for other functions as well?!? I see no reason to open this pandora box.

Anonymous
Mon 09 Sep 2019 07:22:54 PM UTC, comment #6: 

I would also like to point out that if the LHS value is passed as a double array, then a simple override like this that currently works in Octave would start to fail:

function y = subsasgn (a, s, b)
  a(s.subs{:}) = b;
  y = a;
endfunction

I'm also confused why the 'val' value is passed in as a double array, but a direct call to subsasgn like this with a builtin class fails in Matlab:

y = subsasgn (zeros(1, 10), substruct('()', {10}), inputParser())

The following error occurred converting from inputParser to double:
Conversion to double from inputParser is not possible.

Mike Miller <mtmiller>
Project Administrator
Mon 09 Sep 2019 06:48:53 PM UTC, comment #5: 

I'm changing the summary line to reflect what I think this bug is now about.

At best this is undocumented internal Matlab behavior, and at worst, this is an accident of the implementation that a user shouldn't rely on. It seems prudent to me to avoid relying on the type or dimensions of the left hand side value, other than to do the assignment that the subsasgn call is providing.

But if this is an important feature to you or others, a good first step would be to work on a patch changing this behavior to show that it's feasible and safe to make this change. I can't guarantee that it will become part of Octave, but it's always worth doing the work if the change is important enough to you.

Can you give a concrete example showing why it would be important to the user to have an array of double instead of objects in this scenario? That might help someone see the relative importance of this behavior, or to suggest alternative workarounds to you.

Mike Miller <mtmiller>
Project Administrator
Thu 05 Sep 2019 06:46:53 AM UTC, comment #4: 

Interesting!
Below is the output of matlab R2018b: val is initialized as an empty array of type 'double'. In the latest code used for this, I print the result of 'size(x)':

>> clear x
>> a = test
calling the test constructor

a =

Calling test.size.
  test with no properties.

>> x(10) = a
Calling test.subsasgn(val, idx, rhs).
val is of type 'double':

val =

     []

idx is of type 'struct':

idx =

  struct with fields:

    type: '()'
    subs: {[10]}

rhs is of type 'test':

rhs =

Calling test.size.
  test with no properties.

Now we check the size of 'val':
     0     0

Output argument "result" (and maybe others) not assigned during call to "test/subsasgn".

Note that:
 - The call to 'test.size' was made by matlab while printing the properties of 'rhs', not when asking for the size of 'val'
 - The last error is just here because the function 'subsasgn' is not fully implemented in this example

Still, it seems that matlab follows what you found in the documentation. Indeed, when a class does not come with its own implementation of the function 'subsasgn', it initializes the destination as a table of objects. Here is another matlab output that is similar to what you get in octave:

>> clear all
>> b = testing
calling the testing constructor

b =

Calling testing.size.
  testing with no properties.

>> y(10) = b
calling the testing constructor

y =

Calling testing.size.
Calling testing.size.
  1×10 testing array with no properties.

Therefore, my impression is that the help describes the behavior of matlab's implementation of 'subsasgn' (which naturally extends to any class which does not implement this function explicitly), and if a class comes with its own implementation, then matlab leaves it to the implementation to decide what it wants to do. Then it simply transfers the arguments it received to that function to do the job: in this case it seems that an empty array is transferred as an empty array of type double.

Anonymous
Wed 04 Sep 2019 10:44:27 PM UTC, comment #3: 

Ok, I see the new example does what you describe.

I also think that this example agrees with the Matlab documentation. Specifically, on this page, it says

> If A does not exist before you execute the assignment statement, then MATLAB initializes the five array elements that come before A(2,3) with default objects of class B.


To my reading, that says that 'val' should indeed be an object array of type 'test' when the subsasgn method is called assigning a new instance of 'test' into a non-existent array. So I think that Octave is still behaving correctly.

Do you have any data, documentation, or example output from Matlab (and which version of Matlab) showing a different behavior?

Mike Miller <mtmiller>
Project Administrator
Wed 04 Sep 2019 09:38:36 PM UTC, comment #2: 

Oh, you're right the example did not illustrate the issue with 'x' being mistakenly replaced by 'this'. It seems like I wrote this short example too quickly, sorry about that.

Attached is a new version which hopefully works this time. The problem arises this time with the following code:
  clear x % just making sure x is not yet assigned
  a = test
  x(10) = a

The situation is slightly more elaborated: an object of class 'test' is assigned onto an empty 'double' array. This rightly triggers a call to 'test.subsasgn(val, idx, rhs)', with parameters:
  val = x
  rhs = a

However, from within the function 'test.subsasgn', the variable 'val' is suddenly not an empty double array, but rather it has become an object of type 'test', more precisely a '<object array test>'. rhs has remained itself, an object of type test, more precisely a '<object test>'.

calling 'size(val)' then triggers the function 'test.size' instead of calling the size function for (empty) double arrays.

(file #47454)

Anonymous
Wed 04 Sep 2019 07:49:08 PM UTC, comment #1: 

I'm confused by your example. In the test.func method, you create an array 'x' of double, but then you call 'size(this)', which does and should call the test.size method because the argument is a 'test' object. So that seems to be behaving correctly. If you change the example to call 'size(x)', then it does correctly call double.size. Do you agree that there is no bug here?

Mike Miller <mtmiller>
Project Administrator
Wed 04 Sep 2019 08:11:10 AM UTC, original submission:  

It seems that the dispatching of overloaded functions is overridden by the presence of files in the current directory, even when the current folder is a class folder.

In the attached example, a class 'test' is defined. This class has a 'size' function, to be used for objects of type 'test'. But when a method of the 'test' class calls the 'size' function on an object of a different type, Octave tries to use this class method 'test.size' on this object of a different type, instead of the standard 'double.size' function. This results in an unexpected error.

Calling the following two commands illustrates the problem with text outputs:
  a = test
  func(a)

This behavior clearly differs from Matlab's, and creates code which is location-specific: a function can produce a different result if it is moved into a different folder.

Anonymous

 

(Note: upload size limit is set to 16384 kB, after insertion of the required escape characters.)

Attach Files:
   
   
Comment:
   

Attached Files
file #47456:  test3.zip added by None (2KiB - application/zip)
file #47454:  @test2.zip added by None (988B - application/zip)
file #47447:  @test.tar.gz added by None (479B - application/gzip)

 

Depends on the following items: None found

Items that depend on this one: None found

 

Carbon-Copy List
  • -email is unavailable- added by None (Submitted the item)
  •  

    Do you think this task is very important?
    If so, you can add your encouragement to it.
    This task has 0 encouragements so far.

    Only project members can vote.

     

     

     

    Follow 7 latest changes.

    Date Changed by Updated Field Previous Value => Replaced by
    2019-09-09 mtmiller CategoryNone => Interpreter
        Priority5 - Normal => 3 - Low
        Item GroupUnexpected Error => WTF, Matlab?!?
        Summaryclass method calls overloaded function instead of basic function => Matlab calls class subsasgn method with array of double instead of class type
    2019-09-04 None Attached File- => Added @test2.zip, #47454
    2019-09-04 mtmiller StatusNone => Works For Me
    2019-09-04 None Attached File- => Added @test.tar.gz, #47447

    Back to the top


    Powered by Savane 3.5