7
$\begingroup$

V 12.1 on windows 10.

I am still learning how to use Associations.

This is very strange. I wanted to change field in association using AssociateTo. When the association is inside a list, the replacement does not work. This is better shown with MWE

ClearAll[y, x];
ode1 = <|"ode" -> y[x] == 0, "y" -> y, "x" -> x|>;
ode2 = <|"ode" -> y[x] + x == 1, "y" -> y, "x" -> x|>;
sol  = {y[x] == 999, y[x] == -20};
ODEs = {ode1, ode2};  (*list of Associations *)

Now to replace field ode in ode1 by y[x] == 999, I did

AssociateTo[ ODEs[[1]], "ode" -> sol[[1]] ]

But this gives

 {<|"y" -> y, "x" -> x, "ode" -> y[x] == 999|>, 
  <|"ode" -> x + y[x] == 1, "y" -> y, "x" -> x|>}

Notice it returned not just the first part of the list, but it also returned back ODEs[[2]] with it!

This causes big problems. (Example of big problem is given below if needed)

But when doing

 AssociateTo[ ode1, "ode" -> sol[[1]]]

It works, and returns the expected change to the association

 <|"ode" -> y[x] == 999, "y" -> y, "x" -> x|>

But I want to do this change when the Associations are inside a list.

Question is: When does AssociateTo[ ODEs[[1]], "ode" -> sol[[1]] ] return all contents of the list and not just the part affected?

Appendix

Example why the above behavior is causing a problem is this. MapThread fails now

 ClearAll[y,x];
 ode1     = <|"ode" -> 5 == y[x], "y" -> y, "x" -> x|>;
 ode2     = <|"ode" -> 5 == y[x] + x, "y" -> y, "x" -> x|>;
 sol      = {y[x] == 19, y[x] == 29};  (*new values to update with *)
 ODEs     = {ode1, ode2};  (*list of Associations *)

 MapThread[ AssociateTo[#2, "ode" -> #1] &, {sol, ODEs}]

Mathematica graphics

And I think this error is related to the main question above.

I tried Evaluate, and looked at How does MapThread work with Associations and looked at AssociationThread but so far no solution I could see for the main question above.

I can for now work around this as follows

Last@Reap@Do[
   tmp = ODEs[[n]];
   Sow[ AssociateTo[tmp, "ode" -> sol[[n]]]]
   ,
   {n, 1, Length[ODEs]}
   ]

Which gives what I want

 {<|"y" -> y, "x" -> x, "ode" -> y[x] == 999|>, 
  <|"ode" -> y[x] == -20,"y" -> y, "x" -> x|>}

References

https://reference.wolfram.com/language/ref/AssociateTo.html

https://reference.wolfram.com/language/ref/Association.html

https://reference.wolfram.com/language/ref/AssociationThread.html

$\endgroup$
10
$\begingroup$

To the question in the title, AssociateTo does work with an Association in a list. It works in that it modifies the association in place.

In[67]:= assocs = {<|a -> b|>, <|c -> d|>};
AssociateTo[assocs[[1]], <|f -> g|>];
assocs[[1]]

Out[69]= <|a -> b, f -> g|>

It does seem odd that AssociateTo returns the entire list rather than the modified element.

MapThread failing is the correct behavior, it's exactly the same error you would get from

In[51]:= list1 = {Range[4], Range[4]};
list2 = {a, b};

In[53]:= MapThread[AppendTo, {list1, list2}];

During evaluation of In[53]:= AppendTo::rvalue: {1, 2, 3, 4} is not a variable with a value, so its value cannot be changed.

The solution with lists is to use Append instead of AppendTo, and that works for your associations as well,

In[62]:= MapThread[Append[#2, "ode" -> #1] &, {sol, ODEs}]

Out[62]= {<|"y" -> y, "x" -> x, "ode" -> y[x] == 19|>, <|"y" -> y, 
  "x" -> x, "ode" -> y[x] == 29|>}

If you want to modify the list of associations in place, you can take advantage of this syntax:

In[83]:= assocs = {<|a -> b|>, <|c -> d|>};
assocs[[1, Key[m]]] = n;
assocs

Out[85]= {<|a -> b, m -> n|>, <|c -> d|>}

If m were a string above you would not need to wrap it in Key before using it as a part.

So your original MapThread with AssociateTo can be written as

MapIndexed[(ODEs[[#2[[1]], "ode"]] = #1) &, sol]
| improve this answer | |
$\endgroup$
  • 1
    $\begingroup$ Thanks for answer. Are you saying AssociateTo[ode1, "ode" -> sol[[1]]] and Append[ode1, "ode" -> sol[[1]]] are semantically the same? I did not know that Append would work, since help says to use AssociateTo to change field in association. If Append works same as AssociateTo, then why have AssociateTo ? But will use Append for now, since it works :) $\endgroup$ – Nasser Jun 4 at 11:55
  • $\begingroup$ @Nasser - the important distinction is that Append will return a new association but not modify the original one $\endgroup$ – Jason B. Jun 4 at 12:03
  • 1
    $\begingroup$ @Nasser I highly recommend learning the subtle points of using associations, they really are nice. I added a couple points about how to modify them in place. $\endgroup$ – Jason B. Jun 4 at 12:09
  • 1
    $\begingroup$ I am not able to find a way to tell Mathematica to modify the field in association, but return error if the field do not exist. I mean, suppose Association X has fields A,B, and by mistake I typed X[[ Key[A1] ]] = 99 then it will take it, and will add new A1 as new key with value 99. I know one can first query using Lookup first or KeyExistQ to check, but still it will be nice if there is a function such as modifyThisKeyOnlyIfExists type function. This is to avoid by accident adding new key. Right now, Append and AssociateTo do not check if key exist or not either. $\endgroup$ – Nasser Jun 4 at 12:20
  • 1
    $\begingroup$ I put a version of modifyThisKeyOnlyIfExists in the chat room $\endgroup$ – Jason B. Jun 4 at 14:39
5
$\begingroup$

Preamble

I'll throw this out as another approach one could take. It perhaps goes a little beyond the OP's particular question, but I ran into the same problem some time ago. I came up with this approach as the easiest thing I could manage. I'm not an expert on Association/Dataset objects, so I present it only as the best thing I've come up with so far.

First, let me observe that the reason AssociateTo[] is HoldFirst is so that it can find a symbol representing an association to modify. (To me, this makes things awkward in Mathematica, but I am open to being shown that is a result of my lack of expertise.) Evidently, it will do some parsing of the first argument, which is not documented in the AssociateTo page.

Further, I think a complete example would exhibit what happens to the source associations ode1 etc., namely that ODEs is changed but not ode1. This should make sense, since the references to ode1 and ode2 are lost in the definition of ODEs. However, I either read or misread the question to mean that the modification of ode1 and ode2 was desired, probably because that was the problem was trying to solve for myself.

AssociateTo[ODEs[[1]], "ode" -> sol[[1]]]
ODEs
ode1
(*
  {<|"y" -> y, "x" -> x, "ode" -> y[x] == 999|>,
   <|"ode" -> x + y[x] == 1, "y" -> y, "x" -> x|>}

  {<|"y" -> y, "x" -> x, "ode" -> y[x] == 999|>,
   <|"ode" -> x + y[x] == 1, "y" -> y, "x" -> x|>}

  <|"ode" -> y[x] == 0, "y" -> y, "x" -> x|>
*)

Alternative approach

The idea is to wrap the symbol reference to the association in a container that holds its arguments. This can then be used to define operations on the data structure. The form is diffEq[ode], where ode is a Symbol whose value is an Association consisting of the data for the differential equation. You can then use AssociateTo on ode, if you're careful not to let ode evaluate. Then you can define operations like this:

diffEq[ode_]["solution"] := DSolve[ode["ode"], ode["y"], ode["x"]];

There is a method for creating and updating a diffEq[] called setupDiffEq. One could argue that the method for updating should have its own name. Well, you can easily change it.

My data set could be quite large, since it might save things like the results of NDSolve. It was convenient to format it with a summary form, which I've included. I also threw in checkOpts[] to check to see if the rules are valid for our data structure based on some remarks in the comments.

ClearAll[diffEq];
SetAttributes[diffEq, HoldAll];
diffEq /: MakeBoxes[de : diffEq[asc_], form_] /; AssociationQ[asc] :=
  Module[{above, below, ivars},
   ivars = Lookup[asc, "independentVars", Automatic];
   above = {{BoxForm`SummaryItem[{Lookup[asc, "ode", None]}]}};
   below = {};
   BoxForm`ArrangeSummaryBox[diffEq, de, "ODE", above, below, form]];

(* Check that opts are Options of the symbol sym
 *   Returns { status (T/F), filtered good opts } *)
ClearAll[checkOpts];
SetAttributes[checkOpts, HoldFirst];
checkOpts[code_, sym_Symbol, opts : OptionsPattern[]] := 
  With[{oplist = Flatten@{opts}},
   With[{bad = FilterRules[oplist, Except@Options@sym]},
    If[Length@bad > 0,
     Message[sym::optx, First@bad, HoldForm@code];
     {False, FilterRules[oplist, Options@sym]}
     ,
     {True, oplist}
     ]
    ]];

ClearAll[setupDiffEq];
(* Create a diffEq[] from rules *)
call : setupDiffEq[opts : OptionsPattern[]] := Module[{ode},
   With[{opres = checkOpts[call, setupDiffEq, opts]},
    ( (* TBD: validate/process option values *)
      ode = Association[Last@opres];
      diffEq[ode]
      ) /; First@opres
    ]];
(* Change an existing diffEq[] *)
setupDiffEq::optx = "Unknown diffEq key `1` in `2`.";
Options@setupDiffEq = {"ode", "y", "x"};
call : setupDiffEq[de : diffEq[asc_], opts : OptionsPattern[]] :=
  With[{opres = checkOpts[call, setupDiffEq, opts]},
   (AssociateTo[asc, Last@opres]
    ; de
    ) /; First@opres
   ];

Usage:

ode1data = <|"ode" -> y[x] == 0, "y" -> y, "x" -> x|>;
ode1 = diffEq[ode1data]

diffEq[ode1data]

Or with a Module-generated symbol.

ode1 = setupDiffEq["ode" -> y[x] == 0, "y" -> y, "x" -> x]

diffEq[ode29817]

setupDiffEq[ode1, "ic" -> y[0] == 1]

setupDiffEq::optx: Unknown diffEq key ic->y[0]==1 in setupDiffEq[diffEq[ODE y[x]==0],ic->y[0]==1].

setupDiffEq[diffEq[ode1], "ic" -> y[0] == 1]
setupDiffEq[ode1, "ode" -> y[x] == 999]

diffEq[ode1]

OP's example

The data can be specified as lists, but I followed the OP's lead. If you prefer working strictly with associations, you could modify the definition of setupDiffEq or add a definition like setupDiffEq[a_?AssociationQ] := setupDiffEq@Normal@a.

ode1data = <|"ode" -> y[x] == 0, "y" -> y, "x" -> x|>;
ode2data = <|"ode" -> y[x] + x == 1, "y" -> y, "x" -> x|>;
ODEs = setupDiffEq /@ Normal@{ode1data, ode2data}

sol = {y[x] == 19, y[x] == 29};(*new values to update with*)
MapThread[
 setupDiffEq[#2, "ode" -> #1] &, {sol, ODEs}]

| improve this answer | |
$\endgroup$
3
$\begingroup$

Here is a shorter version of your question:

xy = {<|"x" -> x|>, <|"y" -> y|>}
AssociateTo[xy[[1]], "x" -> 1]

The question: why is the result the modified list, rather than the modified part? An answer: you cannot have access to the modified part without the whole list. (Otherise you could then modify this "part" separately from modifying the whole, etc.)

Lesson: avoid in-place modification unless you really need it. In this case, Append[xy[[1]], "x" -> 1] illustrates an approach that meets your needs.

| improve this answer | |
$\endgroup$
3
$\begingroup$

The misunderstanding here seems less related to AssociateTo and more to the difference between immutability and mutability. Yes it is the case that AssociateTo returns the whole association unlike the more localized return of say AppendTo (a possible explanation follows) but this is not the reason behind the observed failure---even if AssociateTo did return the locally changed expression the desired change to ODEs would not take place because mutable changes using functions like AssociateTo, AppendTo etc. require the first (held) argument to (lexically) refer to a variable or its part.

Hence we see that the OP's snippet

AssociateTo[ode1, "ode" -> sol[[1]]]

does perform as desired because ode1 is not evaluated given that AssociateTo holds its first argument. Of course performing this doesn't automatically update ODEs as this variable would need to be re-evaluated so the following attempt fails for this reason alone

MapThread[AssociateTo[#2, "ode" -> #1] &, {sol, ODEs}]

but it also fails because ODEs evaluates not to {ode1,ode2} but to their final values (associations) and hence AssociateTo doesn't have a variable name to hook onto.

The OP's workaround succeeds because the Do effectively injects the part into the AssociateTo[ODEs[[i]], ... ]thereby giving AssociateTo a variable part to hook onto.

While it is tempting to perform the desired setting viz

MapIndexed[(ODEs[[#2[[1]], "ode"]] = #1) &, sol]

and it works well enough here as per Jason B.s solution, I find this to be a somewhat risky general approach because you are relying on the timing and independence of changes to ODEs proceeding without surprises. I've been burned a few times in the past relying on this (say in the frontend when changing dynamical variables where the control flow is less certain or say when mutable changes involve writing to disk) so that IMO a more robust approach is to perform all changes immutably before leaving any persistent (mutable) changes to a final "Set". Fold is handy for this

ODEs = Fold[
      Insert[#1,"ode"->#2[[1]], Append[#2[[2]], 1]] &,
      ODEs, 
      MapIndexed[{#1, #2} &]@sol
       ]

->

{
 <|"ode" -> y[x] == 999, "y" -> y, "x" -> x|>,
 <|"ode" -> y[x] == -20, "y" -> y, "x" -> x|>
}

(naturally efficiency enters the picture depending on the size of the persistent structure and where different approaches may be needed)

Returning to why a whole association is returned when changing internal (no matter how deep) key values unlike for lists where only local changes are returned---I suspect this is down to Association's role in queries where often one needs to select entire rows (pattern matching for Associations works similarly) with perhaps this allowing for the use-case of performing some persistent changes on the fly.

| improve this answer | |
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.