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"]]

enter image description here

  • 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 the volume 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 of dyn next to the slider, but Dynamic[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]]