ReplacePart equivalent for Associations

I have a fairly complicated Association. I wish to use/create a function such as ReplacePart to operate on certain Keys within this multi-level Association.

Here's a toy example:

assoc = <|
"PARAMETERS" -> <|"A" -> 1, "B" -> 2|>,
"PHASES" -> <|
"LIQUID" :> (assoc["PARAMETERS", "B"]*# &),
"GAS" :> (assoc["PHASES", "LIQUID"][#] *0.5* # &)
|>
|>


This Association contains two functions which are used to calculate endless other trivial stuff.

We can use them like so:

Plot[assoc["PHASES", "GAS"][x], {x, -5, 5}]


Now, it's not hard to change one of these functions:

assoc["PHASES", "GAS"] := assoc["PHASES", "LIQUID"][#] * 0.7 * # &


...and this works perfectly fine. But in practice, I might be changing these functions or other values many times and a programmatic way would be preferred.

I'm picturing something like this:

newassoc = ReplacePart[assoc, {{"PHASES", "GAS"} :> assoc["PHASES", "LIQUID"][#] * 0.7 * # &, {"PARAMETERS", "A"} -> 15}]

<|
"PARAMETERS" -> <|"A" -> 15, "B" -> 2|>,
"PHASES" -> <|
"LIQUID" :> (assoc["PARAMETERS", "B"]*# &),
"GAS" :> (assoc["PHASES", "LIQUID"][#] *0.7* # &)
|>
|>


However, it doesn't appear ReplacePart works on Associations, and with my ineptitude at non-standard evaluation, I'm having trouble coming up with a way to do this.

I genuinely hope I've just looked over an easy solution, but any help is greatly appreciated.

ReplacePart did not work with associations in versions 10.0.0 and 10.0.1, but it does as of version 10.0.2. The proposed expression shown in the question will work in 10.0.2 (after making the minor adjustment of putting parentheses around the replacement function):

newassoc =
ReplacePart[assoc
, { {"PHASES", "GAS"} :> (assoc["PHASES", "LIQUID"][#] * 0.7 * # &)
, {"PARAMETERS", "A"} -> 15
}
]

(*
<|PARAMETERS-><|A->15,B->2|>,
PHASES-><|LIQUID:>(assoc[PARAMETERS,B] #1&),GAS:>(assoc[PHASES,LIQUID][#1] 0.7 #1&)|>
|>
*)


MapIndexed might be a passable alternative for ReplacePart in 10.0.1 were it not for the fact that the associations in question contain deferred rules. Consider:

update[v_, _] := v
update[_, {Key["PHASES"],Key["GAS"]}] := assoc["PHASES", "LIQUID"][#] * 0.7 * # &
update[_, {Key["PARAMETERS"],Key["A"]}] := 15

MapIndexed[update, assoc, {2}]

(*
<|PARAMETERS-><|A->15,B->2|>,PHASES-><|LIQUID:>update[assoc[PARAMETERS,B] #1&,
{Key[PHASES],Key[LIQUID]}],GAS:>update[assoc[PHASES,LIQUID][#1] 0.5 #1&,
{Key[PHASES],Key[GAS]}]|>|>
*)


Note how the simple rule values were updated successfully, but the deferred phase functions are left with unevaluated calls to update. This complication is difficult to fix without converting the association to normal form and back. Note, however, that in the case at hand there is no need to defer the function properties as their values are already inert. So unless the "real" system requires these deferred rules, MapIndexed might be workable.

• Glad to see this was remedied in 10.0.2. I'll be contacting my IT folks tomorrow. And yes, the deferred functions are necessary as the phase function are sometimes dependent on one another and may not be defined in order of dependence. Thanks!
– kale
Dec 15, 2014 at 23:32
• Also the functions may contain random variables or distributions, so I think I'm stuck with the deferred assignments.
– kale
Dec 15, 2014 at 23:45

You wrote:

Now, it's not hard to change one of these functions:

assoc["PHASES", "GAS"] := assoc["PHASES", "LIQUID"][#] * 0.7 * # &


...and this works perfectly fine. But in practice, I might be changing these functions or other values many times and a programmatic way would be preferred.

Perhaps a meta-programming approach would be useful? One might do something like this:

SetAttributes[sub, {HoldFirst, Listable}]

Off[Pattern::patv] (* turn off non-error message *)

sub[asc_, (Rule | RuleDelayed)[{LHS__} | LHS_, RHS_]] := asc[LHS] := RHS


Now:

sub[
assoc,
{
{"PHASES", "GAS"}   :> (assoc["PHASES", "LIQUID"][#]*0.7*# &),
{"PARAMETERS", "A"} -> 15
}
];

assoc

<|"PARAMETERS" -> <|"A" :> 15, "B" -> 2|>,
"PHASES" -> <|"LIQUID" :> (assoc["PARAMETERS", "B"] #1 &),
"GAS" :> (assoc["PHASES", "LIQUID"][#1] 0.7 #1 &)|>|>


This has the desired evaluation behavior with either Rule or RuleDelayed, or a combination of both in a List, but all inserted Associations will use :> even though the RHS will be pre-evaluated. As kale himself notes one could instead write two definitions:

SetAttributes[sub, {HoldFirst, Listable}]

Off[Pattern::patv] (* turn off non-error message *)

sub[asc_, ( {LHS__} | LHS_ ) -> RHS_] := (asc[LHS] =  RHS;)
sub[asc_, ( {LHS__} | LHS_ ) :> RHS_] := (asc[LHS] := RHS;)


Now the output from the example above will be:

<|"PARAMETERS" -> <|"A" -> 15, "B" -> 2|>,
"PHASES" -> <|"LIQUID" :> (assoc["PARAMETERS", "B"] #1 &),
"GAS" :> (assoc["PHASES", "LIQUID"][#1] 0.7 #1 &)|>|>


Note the -> in "A" -> 15.

The pattern {LHS__} | LHS_ is used in each case to allow the following syntax:

sub[assoc, "PARAMETERS" -> "foo"]

<|"PARAMETERS" -> "foo",
"PHASES" -> <|"LIQUID" :> (assoc["PARAMETERS", "B"] #1 &),
"GAS" :> (assoc["PHASES", "LIQUID"][#1] 0.7 #1 &)|>|>

• Nice. Mine gripes about LHS being used in both fixed and variable length patterns, but whatever. I'd make one change: two definitions for Rule and RuleDelayed -- sub[asc_, RuleDelayed[{LHS__} | LHS_, RHS_]] := asc[LHS] := RHS sub[asc_, Rule[{LHS__} | LHS_, RHS_]] := asc[LHS] = RHS
– kale
Dec 16, 2014 at 21:34
• (+1 by the way.)
– kale
Dec 16, 2014 at 21:36
• @kale Thanks. That "gripe" is needless and can be ignored. (I'll be happy to explain if you want.) Your two-definition form is more correct but in normal use they will behave the same: try mine with e.g. {"PARAMETERS", "A"} -> 2 + 2 and you will see that this evaluates to 4 even though :> is used in the association. Dec 16, 2014 at 23:52
• I figure the pattern error is because Alternatives isn't recognized. And yeah, the way you had it works fine, but to make a true ReplacePart alternative, I'd just like the freedom to decide Rule vs RuleDelayed. Of course, now with 10.0.2, ReplacePart works as I wanted it to and you have just taught me more about meta-programming.
– kale
Dec 17, 2014 at 14:06
• @kale The thing is it is not an error but instead a hand-holding training-wheels warning message telling you that you might have made a mistake. I have this message turned off as I make frequent use of these pattern objects. Please see: (52258). By the way don't forget to Accept WReach's answer, unless you are waiting for something else. Dec 17, 2014 at 17:10

This only addresses your toy example, but what about this approach that avoids ReplacePart:

assoc = <|"PARAMETERS" -> <|"A" -> 1, "B" -> 2|>|> // Dataset

phases["LIQUID"][x_] := assoc["PARAMETERS", "B"]* x
phases["GAS"][x_] := phases["LIQUID"][x]*0.5 *x

Plot[phases["GAS"][x], {x, -5, 5}]


Or even as operators:

phases2["LIQUID"][x_] := Query["PARAMETERS", x * #B &]
phases2["GAS"][x_] :=  assoc[phases2["LIQUID"][x]]*0.5*x