maths / moodle-qtype_stack

Stack question type for Moodle
GNU General Public License v3.0
142 stars 149 forks source link

Support for Maxima structures (and their `@` field access operator) in question variables #1068

Open lukaszkadlubowski opened 11 months ago

lukaszkadlubowski commented 11 months ago

Hello,

What I want to achieve

I have several functions that should return structured data that I want to access in a key-value manner. I tested Maxima structures (https://maxima.sourceforge.io/docs/manual/maxima_singlepage.html#Structures) to achieve that and they work fine in my local Maxima setup.

Problem

The problem is that Maxima structures use a @ field access operator, which seems not to be allowed in question variables field. This is the error I get: The characters @, $ and \ are not allowed in CAS input.

Question

So I would like to ask if there is any way to make Maxima structures work in STACK, or is there any recommended workaround, supported datatype and/or syntax for the scenario where I have more than 10 values to be returned by a function?

Example

Minimum working (in my local Maxima setup) and non-working (in question variables of a Moodle STACK question) example is:

myfunction() := block(
    [retval],
    defstruct (mystruct  (fieldA, fieldB)),
    retval : new(mystruct),
    retval@fieldA: "This is fieldA value",
    retval@fieldB: "This is fieldB value",
    return(retval)
);

x : myfunction();
ta : x@fieldA;
aharjula commented 11 months ago

Currently forbidden, but unlike in older STACK versions, we do not have impossible technical constraints for allowing this. The constraints are more related to author-side debugging messages and how those should represent things. One reason why these might still have some issues in our world is this:

a: mystruct("foo","bar");
defstruct (mystruct  (fieldA, fieldB);
b: new(mystruct);
b@fieldA: "foo";
b@fieldB: "bar";
print(tex1(a));
print(tex1(b));

Basically, from the outside, it is difficult to distinguish between structs and function calls.

As to alternatives

The approach I think is most used is the so-called inert function, like stackunits(num,unit), where things are tied together with a function call that will never be evaluated because the function has no definition. Naturally, this has the problem of dealing with accessing individual fields; fields are not named but instead positional, and it is often simpler to unpack all fields. As an example, lately, I have been playing with representing graphs and in my case, this is a typical access method, i.e. unpack and repack, which is obviously less convenient than that @ operator or updating a list element by index:

ta: directed_graph( [[-0.5,5.5],[-2.5,-2],[4,-2],[-7,1]], [[1,2],[1,3],[2,1],[2,3],[3,1],[3,2],[4,2]] );
[nodes, edges]: args(ta);
/* Naturally, if there were more "arguments", one can access by position to pick singular things. */
edges: args(ta)[2];
/* Then one could simply return that same thing back */
retval: directed_graph(nodes, edges);

In general, lists have proved to be very useful, especially if one knows how to unpack them using that multi-assign operator like on the second line of the example. However, every now and then people need to have maps/dictionaries, and as not everyone is aware of the way the handy assoc-function works there are some utility functions in STACK for creating so-called "STACK-maps". STACK-maps are a bit silly construct, but as we needed a way to distinguish dictionaries and lists in an unambiguous way in our own JSON parser, those were created to fill in that blank.

As to functions returning many things, I think that returning a list is the STACK way of doing things, even our answer tests follow that pattern:

[Errors, Result, FeedBack, Note]: AnswerTest(StudentAnswer, TeacherAnswer, [Opt], [Raw]);

Naturally, if it returns a massive number of things you cannot unpack them selectively in that assignment. Either unpack all or take the result as a list and pick some from that.

As to structs

The path for them to become a thing in STACK is as follows, and I don't really see how it would fit our current development plans as it does not really connect to any of the things in movement currently. But I will keep my eyes open to avoid them becoming impossible again.

  1. Must ensure that defstruct-calls are identified and present in all execution contexts using those structs.
  2. Should keep track of defstruct-calls and ensure that no conflicting definitions are in play.
  3. Tools like answer tests that do not expect structs should include logic to give sensible errors when receiving them.
  4. Can students create structs? Probably not. Can they access fields of structs? Probably not.
  5. Fix all the unit tests related to the char @.
  6. Add @ as an operator to the parser (well the next-gen parser does have it), and allow it in the pre-parsing char filter.
  7. Ensure that one cannot hide function aliasing behind this, i.e. the runtime security checker must be aware of this operator.

Of those number 3 is probably the one that would require the most work.

lukaszkadlubowski commented 11 months ago

Thank you very much for the detailed answer. Actually, I have been using list unpacking already, but as some of my functions would return lists with dozens of elements, relying on the order of items in the list quickly becomes tiresome and the code is difficult to read. Also, assuming I get it right, each variable name used in unpacking needs to be listed in the block([...],) if I want local variable scoping, etc. The whole thing becomes very verbose and error-prone.

I will look at the alternatives you mentioned. It would be nice to have the possibility of using structs sometime in the future.

Thank you for your help and your work on this great tool!