Triggering actions when a variable is set

Some built-in variables trigger actions when their values are changed:

In[17]:= $Assumptions=1 During evaluation of In[17]:=$Assumptions::bass: 1 is not a well-formed assumption. >>
Out[17]= 1

In[18]:= $Assumptions:=2 During evaluation of In[18]:=$Assumptions::bass: 2 is not a well-formed assumption. >>


(Interestingly the message doesn't trigger if it's set again to the same value.)

How can we safely implement something like this? Are the any built-in hooks for value changes?

This is what I currently have. Does it look safe? Does anyone foresee problems? (There's always the least expected thing that goes wrong with these...)

$var /: Set[$var, value_] :=
Block[{$in = True}, Print["$var set to ", value];  $var = value] /; Not@TrueQ[$in]

$var = 6 (*$var set to 6
6
*)


$in would of course be in a private context and the same needs to be done for SetDelayed as well. - Looks safe enough for me. I use the same thing myself when I need this sort of functionality. Of course, I may be unaware of something. Also, if you expose this variable and someone Block-s it, then the printing would not work for assignments to it made inside that Block. – Leonid Shifrin Apr 24 at 19:24 I've used this, too, without issue. To add to what Leonid said, there are a couple of cases where the underlying data can be changed, bypassing Set and SetDelayed, but from what I can tell, those are rare. – rcollyer Apr 24 at 19:39 @rcollyer One of such cases is when one uses direct manipulations with OwnValues:OwnValues[$var] = HoldPattern[$var] :> 1 does not trigger anything of course. The good news is that things like AddTo, Increment etc are top-level, so that the action is triggered. – Leonid Shifrin Apr 24 at 19:40 @rcollyer Another case, perhaps more important in practice, is for part assignments, which are not triggered: $var = Range[10], and then $var[[1]] = 10 goes unnoticed by this triggering mechanism. And in this case UpValues can not be used since $var would be too deep inside Part. –  Leonid Shifrin Apr 24 at 19:46
@LeonidShifrin the Part example is a good one. I was thinking of some experiences I had with SparseArray where I was trying to track changes with this mechanism, and it wasn't captured. –  rcollyer Apr 24 at 19:49

General thoughts

I think that your mechanism is reasonably robust for common use cases, but not fully robust if one wants to take into account all possible ways that the value (or, generally, global properties) of the symbol can be changed in Mathematica.

My current opinion is that making such triggering mechanism fully robust without new system support is very hard or may not be possible at all. Such system support may exist but just not be known to me, however. One place to look at are functions InternalValueChangeVeto and InternalAddHandler["VetoableValueChange", myVetoFunction]. I do know how to make them work with DownValues, but not with OwnValues, though.

Note that, to make your current mechanism more robust, you will have to add similar definitions not just to SetDelayed, but also to TagSet and TagSetDelayed. The interesting thing about them is that while the definitions they create may end up being exactly the same as if you used Set or SetDelayed, they use independent assignment mechanism, so assignment like

$var /:$var = 1


Some analysis

One way to more or less systematically analyze this issue is to look at all built-in functions with Hold* attributes. For functions in the System context, one can use something like

Select[
Names["System*"],
MemberQ[
ToExpression[#, StandardForm, Attributes],
HoldAll | HoldFirst | HoldRest | HoldAllComplete
] &
]


I have manually selected a number of functions from this list which affect, directly or indirectly, some of the global properties of the symbol in question. These functions can be divided into several groups according to their actions. Let us list cases where your mechanism does and does not work, based on such division:

• Direct assignment operators: Set, SetDelayed, TagSet, TagSetDelayed (and possibly also UpSet and UpSetDelayed, depending on whether or not you want to account for changes they can introduce). Here also belong UnSet and TagUnset.

These functions you can account for explicitly, using definitions similar to what you posted.

• In-place modification operators, such as AddTo, AppendTo, Decrement, DivideBy, Increment, PreDecrement, PreIncrement, PrependTo, SubtractFrom, TimesBy.

Luckily, these are implemented using some of the more fundamental assignment operators, so they are all triggered via your method.

• Operators which clear or remove symbols: Clear, ClearAll, Remove, Context. Whether to include or exclude these is a matter of choice, but all of them are not triggered by your mechanism, but you could add more rules to cover these ones too, in analogy with Set. I added Context here, because you can change the context of your variable via

Context[$var] = "NewContext"  and this effectively removes the variable from its current context. • Functions providing lower-level access to symbol's properties:Attributes, DefaultValues, DownValues, FormatValues, Messages, OwnValues, ClearAttributes, SetAttributes, SubValues, UpValues. Again, depending on how general you want to be, you can include only some of them, but at least you will have to include OwnValues. Direct assignments involving these functions will not trigger your mechanism, e.g.: OwnValues[$var] = HoldPattern[$var] :> 10  and covering these cases does not seem possible using the UpValue-based method. • Functions such as Definition, FullDefinition, LanguageExtendedDefinition, or LanguageExtendedFullDefinition can be assigned to (as you have enlightened us yourself here), for example as var1 = 10; LanguageExtendedFullDefinition[$var] =
LanguageExtendedFullDefinition[var1] /. HoldPattern[var1] :> $var  and such assignments will not be triggered by your method. Again, UpValues won't probably help here. • Block: it blocks all definitions attached to a symbol, so if the symbol is Block-ed, assignments inside Block will not be triggered. • Part assignments: these will not be triggered either. For example: $var = Range[10];
$var[[1]] = 100  This one also seems beyond the reach of the UpValues- based method. I am fairly sure that I missed some more possibilities of value changes for symbols. Some functions in Developer, Experimental, Internal, Language and possibly some other contexts may introduce yet more ways to change the values. Conclusions I think that what you've got can be made good enough with a modest amount of additional rules, and will cover many or most common use cases. However, making this fully robust requires dedicated system support. It may be that such API already exists somehere in one of the contexts I mentioned earlier, but is just not known to me. As I mentioned already, InternalValueChangeVeto and InternalAddHandler["VetoableValueChange",...] may be the right ones to look at, but I don't fully know how they work. - add comment Will some dynamic chart fit your needs? I tried using dynamic things to track the change of variable values as followed. First of all I need to filter out those context I'm not interested in. Unfortunately I don't know how to get built-in contexts, so I used a silly way, that is to run some random code before reading contexts: Names["*x"]; {Plot[x, {x, 0, 1}, ImageSize -> 5, PlotStyle -> Darker[Green]], Plot3D[x y, {x, 0, 1}, {y, 0, 1}, ImageSize -> 5], Grid[{{1}}], Dynamic[]} SelectionMove[EvaluationNotebook[], Previous, Cell]; FrontEndExecute[FrontEndToken["Clear"]];  Then I stored a default context set: userMonitor$defaultContexts = Complement[Contexts[], {"Global"}];


and default Global symbol names:

userMonitor$defaultNames = Names["Global*"]; userMonitor$defaultNames =
Join[userMonitor$defaultNames, {"userMonitor$defaultContexts",
"userMonitor$DefOld", "userMonitor$DefNow", "userMonitor$Trigger", "userMonitor$Var", "userMonitor$Var$", "userMonitor$names", "userMonitor$ContextVarSet", "userMonitor$ContextVarSet$",
"userMonitor$Contexts", "userMonitor$VarSet"}];


Then I dynamically update a variable set:

Dynamic[userMonitor$VarSet = {#, StringSplit[#, ""][[-1]] & /@ Names[# <> "*"]} & /@ (userMonitor$Contexts =
Complement[Contexts[], userMonitor$defaultContexts, {"DataPacletsCommonDump", "ProgrammingDump", "WrappersDump"} ]) /. {"Global", userMonitor$names_} :>
{"Global",
Complement[userMonitor$names, userMonitor$defaultNames]},
UpdateInterval -> 0]


And a trigger update slowly, so I can compare current values with old values while keeping them up-to-date:

Dynamic[userMonitor$Trigger = RandomReal[], UpdateInterval -> .5]  Then I can setup a dynamic chart to reflect changes of variable values: DynamicWrapper[ Dynamic[ Function[userMonitor$ContextVarSet,
{Style[userMonitor$ContextVarSet[[1]], Bold], If[# == {}, {{"", ""}}, Function[userMonitor$Var,
Block[{userMonitor$DefNow}, userMonitor$DefNow = ToString[Definition[#]] &[ userMonitor$ContextVarSet[[1]] <> userMonitor$Var];
{userMonitor$Var, Graphics[{ If[SameQ[userMonitor$DefNow, userMonitor$DefOld[ userMonitor$ContextVarSet[[1]] <> userMonitor$Var]], Darker[Green], Red], Disk[]}, ImageSize -> 50] } ]] /@ # ] &@userMonitor$ContextVarSet[[2]] // Grid[#\[Transpose], Frame -> All, ItemSize -> Full] &} // Column
] /@ userMonitor$VarSet // Column, UpdateInterval -> 0], userMonitor$Trigger;
Clear[userMonitor$DefOld]; Set[userMonitor$DefOld[#], ToString[Definition[#]]] & /@
Flatten[ Function[userMonitor$ContextVarSet, userMonitor$ContextVarSet[[1]] <> # & /@ userMonitor$ContextVarSet[[2]]] /@ userMonitor$VarSet],

TrackedSymbols :> {userMonitor\$Trigger}]


Hope this helps.

-
You're right, Dynamic also watched for value changes, and it catches all of them. But in this case I needed something that was completely independent of the front end, and would work in the kernel only as well. –  Szabolcs Apr 25 at 15:12
@Szabolcs I see. Sorry for misunderstanding :( I don't know nothing about kernel.. –  Silvia Apr 25 at 15:20