7
$\begingroup$

I'd like to be able to listen to a variable and know when it's been changed and accept/reject the change as well as use it as an update listener for Dynamic how can I do this?

$\endgroup$
2
  • $\begingroup$ Do you need this functionality contained in the Manipulate or is something external to the Manipulate updating the variable? If external then is it in the same Module`DynamicModule` or elsewhere? $\endgroup$
    – Edmund
    Sep 26, 2018 at 13:13
  • $\begingroup$ @Edmund I was imagining this would be a pure kernel thing without reference to Manipulate or Dynamic (although usable by the latter). $\endgroup$
    – b3m2a1
    Sep 26, 2018 at 14:16

2 Answers 2

11
$\begingroup$

As part of handling this for myself I wrote a package that does it. The idea was to mimic the *Var() classes in tkinter. I'll do a quick demo then explain how I built this, which is the actually interesting part of this.

First we'll load that package and make one of these Listener objects:

Get["https://github.com/b3m2a1/mathematica-tools/raw/master/Listeners.m"]

var=Listener["b"];
var//InputForm
(*Out: Listener["b"] *)

It looks like nothing happened, but we'll look at the proper display form:

displayedForm[expr_] :=
 First@FrontEndExecute@
   ExportPacket[Cell[BoxData@ToBoxes@expr], "PlainText"]

displayedForm@var
(* Out: "Null" *)

So the object is displaying as Null.

Now we'll assign something new to var:

var = 1;
displayedForm@var
(* Out: "1" *)

OwnValues@var//InputForm
(* Out: {HoldPattern[var] :> Listener["b"]} *)

And now even though it displays as 1 the value of var hasn't changed at all.

Next we'll attach a callback that simply tells us what kind of change is being applied to this Listener object:

var["Callback"]=Print[#]&;
var=2
(* Print: Update *)
(* Out: 2 *)
var={1}
(* Print: Update *)
(* Out: {1} *)
var[[1]] = 5
(* Print: UpdatePart *)
(* Out: 5 *)

This callback is telling us the type of change applied. It's also passed the name ("b" in this case) and the args of the change and if it returns False the change is rejected.

Now we'll get to the original reason this was made, which is for forcing updated in Dynamic. If we use:

ListenerDynamic[RandomReal[], var]

every time var is updated the argument (RandomReal[]) gets re-evaluated.

Updates to var, while slow relative to standard variable assignment, are still faster than box updates, so we can the classic:

ListenerDynamic[var = RandomReal[], var]

And watch it tick away. I haven't supported Increment and AddTo and things, so don't get too crazy with it, but it would be easy to add.

Unlike Dynamic[x = RandomReal[]], though, we can turn this off by setting: var["Callback"] = False &

So, great, we got what we came for. We effectively have a Tk *Var* object.

Implementation

But what's fun about this is how it's done. I didn't really do anything fundamentally different than my OOP work here but I used a completely different backend that could have been handled in a very different way to very different effect.

Language`ExpressionStore

To implement this I used the new Language`ExpressionStore. These are "weak" hash maps in that they store values without messing with the ref-counts of stored object and they use a specific object instance as a key and when that object is destroyed all its associated keys and values in the map are destroyed.

There's an example of this in the linked question, but we can see how it works here. First we'll make a new object and get the variable it uses for storing state:

$HistoryLength = 0 (* to prevent ref count changes from Out *)
obj = Listener["object"];
obj["Variable"]

(* Out: Hold[Listeners`Private`listener$21210] *)

This Symbol is what is doing all the state holding and is what that ListenerDynamic is looking for updates on.

Now we'll do some memory tests. First we'll create a large array any attach it to the object:

mem1 = MemoryInUse[];

obj = RandomReal[{-1, 1}, {100, 100, 100}];
mem2 = MemoryInUse[];
mem2 - mem1

(* Out: 8000368 *)

And then we'll simply remove any reference obj as a variable has to its held Listener object in a way where I couldn't have even attached UpValues to mess with things:

OwnValues[obj] = {};
mem3 = MemoryInUse[];
mem3 - mem1

(* Out: -1128 *)

And we see that all the memory associated with obj has been freed. And if we check the definitions on that storage symbol:

Listeners`Private`listener$21210

(* Out: Listeners`Private`listener$21210 *)

We see it cleaned itself up. This is because it was a Temporary symbol and once there were no references to the Listener object that was its key in the underlying ExpressionStore both of them were cleaned up.

Language`MutationHandler

We use ExpressionStore as our backend and then as the interface (front-end?) to the object we use the Language`*Mutation* functions to allow us to cleanly allow var = x to call a custom setter instead of simply calling Set on var.

System`Private`*Entry*

I also use the System`Private`*Entry* functions as a way to test whether my objects have truly been "constructed" or not. If we get a raw Listener[...] expression we can check whether System`Private`HoldEntryQ is true or not and if it is we call a constructor that will return a Listener[...] object that looks the same but is a fundamentally different object with System`Private`HoldSetNoEntry called on it.

Object lookup

I wanted to be able to just use Listener[name] raw without ensuring I had the variable bound as a convenience, so I actually had to check whether an object with that name had already been built. I did this by searching the keys in the expression store I built (Listeners`Package`$Listeners). This is slightly inefficient, but it only happens in the construction step, so I thought the time hit was worth taking.

New forms of OOP

One nice thing about this form of OOP is that it allows us to have more opaque objects than I used in my SymbolObjects package. We could simply use ExpressionStore as a way to store all fields and methods. The memory will be naturally cleaned when whatever object we return from our constructor goes out of scope.

This is both cleaner and potentially more robust than writing interfaces to Symbol or Association.

I wouldn't be surprised to see a lot more development from WRI and package developers around here in that vein.

$\endgroup$
4
$\begingroup$

You may use PersistentValue with its ValuePreprocessingFunction option.

For some listening function such as

listen = Echo;

included in a validation function

validateInteger[x_] := If[IntegerQ[x], listen@x; x, $Failed]

then a PersistentValue can be created which is validated and notifies updates via the ValuePreprocessingFunction.

PersistentValue["int", PersistenceLocation["KernelSession"], 
  ValuePreprocessingFunction -> validateInteger] = 0;

» 0

When set to a value that validates the listen function is called and the PersistentValue updated.

PersistentValue["int", "KernelSession"] = 6

» 6

6

When set to a value that does not validate the listen function is not called and the PersistentValue is not updated.

PersistentValue["int", "KernelSession"] = 6.5
$Failed
PersistentValue["int", "KernelSession"]
6

Clean up with

DeleteObject[PersistentObjects["int"]];

Hope this helps.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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