Modified “Plus” function: how to save just the final result of an addition of multiple objects?

In my version of object-oriented programming in Mathematica, essentially (without going into details) obj[a] is a reference to an object whose actual data is stored inside data[a]. Methods like obj[a][function] then use and/or modify that data.

Schematically, I define addition of objects using UpValues as follows:

obj[a_] + obj[b_] ^:= add[a, b];


The add function looks at the values stored in data[a] and data[b], combines them in the desired way, and then stores them in data[ui] where ui is a new unique identifier (with i an integer) and returns obj[ui].

Now, here is my problem. Consider a long chain of additions, e.g.

obj[a] + obj[b] + obj[c] + obj[d]


This creates 3 new objects in total, obj[u1], obj[u2], and obj[u3], since each addition of two objects creates a new object. Schematically:

obj[a] + obj[b] + obj[c] + obj[d]
= obj[u1] + obj[c] + obj[d]
= obj[u1] + obj[u2]
= obj[u3]


If many objects are added, this creates an abundance of new objects that I don't need. After the addition is over, I would like to clear all of them except the last one, which is the actual result of the expression.

Just clearing all of the obj[ui] for i less than the current index every time I add two objects won't work, for two reasons:

1. Older intermediate steps are still in general needed for the next steps. For example, In the addition I showed above, if the addition obj[c] + obj[d], which results in obj[u2], clears obj[u1], then I won't be able to add obj[u1] + obj[u2] to produce obj[u3].
2. If I perform another (separate) addition later, then I don't want it to delete the results of the previous addition.

Therefore, I need to somehow know where the instance of add[a, b] that is currently running is located in the chain: is it the first, the last, or an intermediate step. If it's the first, I mark the current i as the beginning of the objects to delete. If it's the last, then I delete all of the objects created from that point until i - 1 where i is the current step.

A simpler option is just to know that the instance is the last step, and then rename the object to something like obj[vi] where vi is a new unique identifier and delete all of the obj[ui].

Is that possible? If not, what are some other ways in which I can avoid keeping all the objects that were created in the intermediate steps?

EDIT 1:

I am also doing something similar with multiplication, i.e. obj[a_] * obj[b_] ^:= multiply[a, b], so the solution will need to incorporate combinations like obj[a] + obj[b]*obj[c], which significantly complicates things.

EDIT 2:

The solution I am currently considering is to have the user enclose any operation with some kind of wrapper (e.g. cleanup[ obj[a] + obj[b]*obj[c] ]) and have cleanup[] rename its argument to obj[vi] and delete all the existing intermediate steps obj[ui]. This makes the syntax slightly less elegant for the user, but doesn't require any complicated pattern matching.

It would be nice if there was some way to have the wrapper execute automatically when a cell ends its evaluation, or something like that, in which case the user won't have to worry about the wrapper whenever they make an addition/multiplication operation.

Maybe something along these lines?

obj /: Plus[args : obj[_] ..] := add @@ {args}[[All, 1]];
obj[a] + obj[b] + obj[c]


This way you can write your own multi-argument version of add that encapsulates all of the internal juggling with objects and then cleans up at the end.

Alternatively, if you want to just apply add repeatedly to pairs of arguments:

obj /: Plus[args : obj[_] ..] := Fold[add, {args}[[All, 1]]];
obj[a] + obj[b] + obj[c]


Edit

About your question about mixing Times and Plus: You can make a pattern like Plus[args1___, x : obj[_], args2___] (and similarly for Times) to interrupt normal evaluation the moment an object is found as an argument for one of these functions. You can then pull out the arguments and inspect them further (see if args1 has Times[___] in it, for example). However, if you have something like:

Plus[Times[obj[a], obj[b]], Times[obj[c], obj[d]]]


there is no way you can interrupt at the level of Plus with UpValues because those can only "pop up" one level. In that case, one of the Times expressions will first have to resolve to obj[_] before the Plus upvalue can fire.

Edit 2

Your suggestion of a cleanup wrapper could be done like this as well. For example, you could do:

Plus[args1___, x : obj[_], args2___] /; !TrueQ[wrappedQ] := Block[{
wrappedQ = True
},
cleanup[Plus[args1, x, args2]
]


The wrappedQ variable prevents recursion of this definition. This way you don't need to manually enforce the use of the cleanup wrapper.

• Thanks! Seems like this is a good workaround for addition, but the problem is that I am doing not just addition but also multiplication (I didn't mention this in the question since I didn't want to complicate things). So I think this would work if I'm just adding things, but it won't work if I'm doing something like obj[a] + obj[b]*obj[c] since I won't be able to take into account all possible combinations of Plus and Times. Any suggestions? – user73765 Jul 20 '20 at 18:56
• In that case, you should probably match Plus and Times with any object whatsoever occurring as an argument at any position. E.g., Plus[args1___, x : obj[_], args2___]. That way you can interrupt the normal flow of evaluation and pull things apart any way you need. – Sjoerd Smit Jul 20 '20 at 19:05
• Thanks, that might work, but it sounds pretty complicated and prone to errors - if I miss something, the user will not get the desired behaviour with sufficiently complicated expressions. The solution I am already considering is to have the user enclose any operation with some kind of wrapper (e.g. cleanup[ obj[a] + obj[b]*obj[c] ]) and have cleanup[] rename its argument to obj[vi] and delete all the existing intermediate steps obj[ui]. That makes the syntax slightly less elegant for the user, but doesn't require any complicated pattern matching. – user73765 Jul 20 '20 at 19:20
• @QuantumField See my new edit. You could use Block to automatically wrap the whole expression in cleanup. – Sjoerd Smit Jul 20 '20 at 19:57