9
$\begingroup$

I'm making a UI widget that controls sound volume on a speaker using a slider, e.g.:

settings = <|"sound" -> <|"volume" -> 0.5|>|>

AdjustSettings[settings_] := DynamicModule[{},
   {
    Slider[Dynamic[settings["sound", "volume"]]],
    Dynamic[settings["sound", "volume"]]
    }
   ];
SetAttributes[AdjustSettings, HoldAll];


AdjustSettings[settings] (** works just fine **)

The slider here directly manipulates the volume field, nested in the global settings variable, which is the intended behavior.

However, if I embed settings inside a speaker variable, it stops working:

speaker = <|"settings" -> settings|>
AdjustSettings[speaker["settings"]]  (** doesn't work **)

Specifically, the slider updates the sound field, and drops the volume field altogether, so that the speaker variable becomes:

<|"settings" -> <|"sound" -> 0.578|>|>

What's going wrong here?

Is there a way to pass a reference to a specific part of an association (rather than its value), and then have the UI dynamically modify that part?

$\endgroup$
10
$\begingroup$

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]}
 ]
$\endgroup$
  • 1
    $\begingroup$ @Mr.Wizard, thanks for your answer. I still do not understand one thing though. If speaker["settings"] is not an expression, and cannot be assigned a value, why does it get modified by the slider without throwing any errors? Specifically, the slider still sets <|"settings" -> <|"sound" -> 0.578|>|>. Sure it's not the volume field that gets modified, but it is still a field of a "bare association", isn't it? $\endgroup$ – verse Jul 20 '15 at 21:48
  • 1
    $\begingroup$ @verse Good question! I think my analysis is not quite correct. I do not have time now but I shall return to this later tonight. Thanks for keeping me honest! $\endgroup$ – Mr.Wizard Jul 21 '15 at 2:51
  • 1
    $\begingroup$ @verse Please see my entirely rewritten answer. I hope I got it right this time. $\endgroup$ – Mr.Wizard Jul 22 '15 at 8:18
  • $\begingroup$ @Mr.Wizard thanks for taking the time to write the whole new answer -- it is very insightful, as always. I have one last clarifying question on this. 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? $\endgroup$ – verse Jul 22 '15 at 17:47
  • 1
    $\begingroup$ @verse Please see the new section for my reply. $\endgroup$ – Mr.Wizard Jul 23 '15 at 7:46
2
$\begingroup$

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]]
$\endgroup$
  • $\begingroup$ Since u is passed to dynamic, I'd use DynamicModule in case this is going to be some interface that should last trough sessions. Moreover, what's worth mentioning, the slider won't be updated if our key value will be updated somewhere else. Sometimes it's not a problem, just pointing possible issues. $\endgroup$ – Kuba Jul 17 '15 at 6:41

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.