General considerations
To my mind, the only robust way to do this is to build some custom object model in Mathematica, and in particular to restrict the way values can be changed to some well-defined route you can control. Because, as it follows from one of the discussions you linked to, there seems to be no reliable way to intercept arbitrary value changes at the level of the core language, that would've been exposed to the user and would work in absolutely all cases ( I will be happy to retract this statement as soon as we collect enough evidence that there are some means (ValueFunction
or some other) which are 100% robust and cover absolutely all routes via which values can be changed).
But even if there is such a function which can trigger changes in all cases, I still think that it would be conceptually wrong to base an observer implementation on it, because you'd leave no space in between the core language and your code, to add more hooks, intermediate layers, etc. The observer pattern is rather closely tied to the OO paradigm, and value change is only one of many events one may want to trigger. The proper way of doing that would involve a full-fledged object model, which is not directly supported by the main Mathematica programming paradigm, primarily because of the different approaches to (im)mutability for the former and the latter.
Example: OCaml-style references
That said, here is a simple example - an OCaml style references emulated in Mathematica. This is a small helper macro:
ClearAll[withCodeAfter];
SetAttributes[withCodeAfter, HoldRest];
withCodeAfter[before_, after_] := (after; before);
and here is the main code:
ClearAll[Ref, $Observers, MakeRef, Observe, RemoveObserver ];
$Observers[_] = {};
SetAttributes[Ref, HoldAll];
MakeRef[expr_] := Module[{sym = expr}, Ref[sym]];
Ref /: Value[Ref[sym_]] := sym;
Ref /: Set[ref : Ref[sym_], new_] :=
withCodeAfter[
sym = new,
Scan[Function[obs, obs[ref, new]], $Observers[ref]]
];
Ref /: Set[lhs_Symbol, rhs_Ref] :=
withCodeAfter[
OwnValues[lhs] = HoldPattern[lhs] :> rhs;
lhs
,
lhs /: Set[lhs, val_] := (rhs = val)
];
Ref /: Observe[ref_Ref, observer_] := AppendTo[$Observers[ref], observer];
Ref /: RemoveObserver[ref_Ref, observer_] :=
$Observers[ref] = DeleteCases[$Observers[ref], observer];
(some bad formatting here is due to the SE editor bug, related to the treatment of the $
symbol). Here is how you could use it:
ClearAll[ref]
ref = MakeRef[12]
(* Ref[sym$775610] *)
ref = 10
(* 10 *)
Value[ref]
(* 10 *)
Observe[ref, observer1 = Print["1.- ", ToString[Unevaluated[#1]], ": ", #2] &]
(* {Print["1.- ", ToString[Unevaluated[#1]], ": ", #2] &} *)
ref = 15
During evaluation of In[432]:= 1.- Ref[sym$775610]: 15
(* 15 *)
Observe[ref, Print["2.- ", ToString[Unevaluated[#1]], ": ", #2] &]
(*
{Print["1.- ", ToString[Unevaluated[#1]], ": ", #2] &,
Print["2.- ", ToString[Unevaluated[#1]], ": ", #2] &}
*)
ref = 20
During evaluation of In[434]:= 1.- Ref[sym$775610]: 20
During evaluation of In[434]:= 2.- Ref[sym$775610]: 20
(* 20 *)
RemoveObserver[ref, observer1];
ref = 30
During evaluation of In[438]:= 2.- Ref[sym$775610]: 30
(* 30 *)
I've made varibles to which we assign Ref-s to have a "sticky" behavior, which simplifies assigment syntax.
It should be clear however, that what I suggested is only an emulation. Note in particular, that the fact that I explcitly overloaded assigment operator to work on symbols, which I assign to store Ref
-s, immediately introduced weaknesses: now my model work for this type of assignments, but not direct DownValue
- based assignments. This is because we came too close to the core language layer: should we always stick to Ref
's API, and this will always work - as long as we follow this protocol.
Example: Extensible SetterBar
Here is another example, where the event-handling will make more sense, since this one will be about UI. There is a SetterBar
standard widget in Mathematica, but it has a number of limitations. You can't add more choices dynamically, once you have constructed it, and it is hard to make it execute some custom action on user's clicking on some tab.
Let's build an extensible SetterBar
, which would allow adding more choices at run-time, as well as adding / removing listeners. I will use Module
's kernel variables, which is generally not considered a good practice. It works for me, but be warned.
So, here goes:
ClearAll[makeExtensibleSetterBar]
makeExtensibleSetterBar[labels_List, handler_: Identity] :=
Module[{instance, setter, current, localLabels = labels, handlers = {handler}},
SetAttributes[instance, HoldAll];
setter :=
SetterBar[
Dynamic[current, (Scan[Function[fun, fun@#], handlers]; current = #)&],
localLabels
];
instance @ addHandler[f_] := handlers = DeleteDuplicates[Append[handlers, f]];
instance @ removeHandler[f_] := handlers = DeleteCases[handlers, f];
instance @ add[lab_] := AppendTo[localLabels, lab];
instance @ remove[lab_] := localLabels = DeleteCases[localLabels, lab];
instance @ replace[lab_ -> new_] :=localLabels = localLabels /. lab -> new;
instance @ getCurrent[] := current;
instance @ setCurrent[lab_] := current = lab;
instance @ getLength[] := Length[localLabels];
instance @ render[] := Dynamic[setter];
instance];
I'll show just a couple of examples of how this can be used. Start with this setter bar object:
sb = makeExtensibleSetterBar[{1, 2, 3, 4}]
(* instance$7475964 *)
To render it, call:
sb@render[]
Now, you can press buttons on the setter bar a few times - so far, nothing happens, of course. Let's now add another choice:
sb@add[5]
You should see now that the new choice has been dynamically added to the UI, making it available for you to click on. Let's make it a bit more interesting and add some event handler:
sb@addHandler[handler1 = Print["Handler1: you picked", #] &];
Now, try clicking on the buttons on the setter bar, and you should see things getting printed as you click. Add another handler:
sb@addHandler[handler2 = Print["Handler2: you picked", #] &]
Now, this one will also print, as you click. You can remove them - say, the first one:
sb@removeHandler[handler1]
And now only the second one will print stuff.
In any case, this example is much closer in spirit to the model which the observer pattern would fit. Because of the enchanced level of encapsulation, it feels more natural to only use the API defined above - it makes no sense to assig anything directly to the object instance itself (which we represent by a symbol we return).
Besides, this is a useful piece of functionality, in its own right, since it significantly extends the capabilities of the built-in SetterBar
.
Notes
The approaches I outlined may work well or not so well, but one would need a more sophisticated object model to make this look & feel more natural. One can build that in Mathematica too, but there will be a number of limitations of such top-level implementations. They've been discussed here and on SO more than once.