Passing an unevaluated part of an association to a function
Reanalysis
My earlier assertions were incorrect or at least incomplete. I now believe the problem in your code originates because of a particular behavior that can be seen in this separate example:
asc = <|foo -> <|bar -> <|baz -> 1|>|>|>
<|foo -> <|bar -> <|baz -> 1|>|>|>
asc[foo][bar][baz] = 2;
asc
<|foo -> <|bar -> <|baz -> 2|>|>|>
asc[foo, bar, baz] = 3;
asc
<|foo -> <|bar -> <|baz -> 3|>|>|>
asc[foo, bar][baz] = 4;
asc
<|foo -> <|bar -> <|baz -> 4|>|>|>
asc[foo][bar, baz] = 5;
asc
<|foo -> <|bar -> 5|>|>
It seems that for assignments to work correctly multiple specifications should not be given within any set of brackets except the left-most. For example with a deeper nested association all of these fail when used in assignment:
asc[a][b, c, d]
asc[a][b, c][d]
asc[a, b][c, d]
Whether this is a bug or follows from known evaluation rules I am not prepared to say. (I have made enough mistakes already!) However we can solve the problem by using either the the full Curry form (asc[foo][bar][baz]
) or the single head from (asc[foo, bar, baz]
). In the case of your example a minor change will work:
SetAttributes[fn1, HoldAll];
fn1[settings_] := With[{dyn = Dynamic[settings["sound"]["volume"]]}, {Slider[dyn], dyn}];
speaker = <|"settings" -> <|"sound" -> <|"volume" -> 0.5|>|>|>;
fn1[speaker["settings"]]
However a problem awaits; consider a deeper nesting and this result:
asc = <|"a" -> <|"b" -> <|"c" -> <|"sound" -> <|"volume" -> 1|>|>|>|>|>;
fn1[ asc["a"]["b", "c"] ] (* multiple failures *)
Tag Missing in Missing[KeyAbsent,c][sound][volume] is Protected. >>
One can either be careful to avoid this situation or we can convert that syntax into flat form. For the latter I propose:
SetAttributes[AdjustSettings1, HoldAll];
(* flatten Currying *)
AdjustSettings1[h_?AssociationQ[a__][b__]] := AdjustSettings1[h[a, b]]
(* uses a "vanishing pattern" *)
AdjustSettings1[settings_[parts__] | settings_] :=
With[{dyn = Dynamic[settings[parts, "sound", "volume"]]}, {Slider[dyn], dyn}]
Hopefully one can now throw any of the forms at this and it should work:
asc = <|"a" -> <|"b" -> <|"c" -> <|"sound" -> <|"volume" -> 1|>|>|>|>|>;
AdjustSettings1[asc["a", "b", "c"]]
AdjustSettings1[asc["a", "b"]["c"]]
AdjustSettings1[asc["a"]["b", "c"]]
AdjustSettings1[asc["a"]["b"]["c"]]
- Be aware that the use of
AssociationQ
does cause evaluation. I chose to use it as I feel this is more robust and in most cases it should not cause problems.
Handling parts by name
If I were to use the
dyn
variable as a reference to thevolume
field of the association, how could I perform operations on it outside of the slider? E.g. if in your example I were to write{Slider[dyn], "Volume: "<> ToString@dyn}
, it would not return the value ofdyn
next to the slider, butDynamic[asc[a, b, c, sound, volume]]
instead. Is there a way around that?
Within the definition I provided above I believe one would need something like:
Dynamic["Volume: " <> ToString @ First @ dyn]
A better approach might be to use an undocumented but longstanding syntax of With
that holds its substitutions; :=
in place of =
:
(* starting with the existing definition above *)
AdjustSettings1[settings_[parts__] | settings_] :=
With[{vol := settings[parts, "sound", "volume"]},
{Slider[Dynamic @ vol], Dynamic["Volume: " <> ToString @ vol]}
]
You can use what I call the "module trick" to bind a local variable to the association returned by speaker["settings"]
, this reduces your problem to the previously solved one and works.
Module[{u = speaker["settings"]}, AdjustSettings[u]]