12
$\begingroup$

I am looking for advice on implementing the following. (It is possible that the answer is that it is simply not a good idea to do this.)

I would like to have an object similar to FittedModel. Let's call its head Obj.

Like FittedModel, it will have properties, e.g.

properties = {42}; (* properties stored here for the sake of this toy example *)

Obj[id_]["Property"] := properties[[id]]

Now let's define obj = Obj[1] and evaluating obj["Property"] will yield 42.

I am looking to make it possible to do obj["Property"] = 137 to set this property.

Here's an attempt that doesn't quite work (let's ignore SetDelayed for now and stick to Set):

Obj /: (Obj[id_]["Property"] = value_) := setObjProperty[id, value]

setObjProperty[id_, value_] := properties[[id]] = value

Now Obj[1]["Property"] = 137 will work as I want it to.

However, obj["Property"] = 256 will not. Instead of changing the property, it will associate the following definition with Obj: Obj[1]["Property"] = 256.

enter image description here

Question: Is there a way to implement this syntax for property setting in a reliable way? I realize that this isn't exactly in the spirit of Mathematica objects being immutable. In my case Obj represents a data structure implemented in C++ and id is a handle to it.


Note the following behaviour of = and :=:

enter image description here

The head of the expression to be set is evaluated by Set (or SetDelayed).


Graph has settable properties which are handled through SetProperty, PropertyValue, etc. I find this syntax very tedious, so I was looking for something simpler.

For my application having mutable state seems very natural, and I think it's not worth avoiding. I have a simulation that has a complex internal state, not fully exposed to Mathematica. The simulation can be stopped and resumed. Parameters can be adjusted when the simulation is stopped.

I think (I may be wrong) that in order to work only with immutable objects, in the natural Mathematica way, it would be necessary to expose the full simulation state to Mathematica and to store it as a pure Mathematica expression (instead of data on the C side). I would then store the full state as an Association, and have a function that runs the simulation and returns a new Association (simulation state), along with data collected during the simulation process. Associations would also give me this easy property-setting syntax for free.

However, exposing the full state may not be worth all the extra work.

$\endgroup$
1
  • 1
    $\begingroup$ Of course all this is not necessary at all to get the work done, it would just be nice to have :-) $\endgroup$
    – Szabolcs
    Jul 16, 2014 at 21:15

5 Answers 5

4
$\begingroup$

Well that's a hairy one. I like it though, as it forced me to think about aspects of evaluation that I am normally oblivious to. Unfortunately that thinking didn't lead to any great insights. My only idea so far is to interrupt evaluation and mess with the Stack as Leonid did for How do you set attributes on SubValues?
I have little experience in this area and I am sure to make a number of blunders before I figure it out, but nevertheless here is a first attempt as a proof of concept.

Starting with your own definitions:

properties = {42};

Obj[id_]["Property"] := properties[[id]]

Obj /: (Obj[id_]["Property"] = value_) := setObjProperty[id, value]

setObjProperty[id_, value_] := properties[[id]] = value

And adding mine:

_Obj :=
  Block[{Obj},
    Obj /: (Obj[id_]["Property"] = value_) := setObjProperty[id, value];
    With[{set = 
      Cases[Stack[_], HoldForm[L_ = R_]?(FreeQ[#, Obj] &) :> ((# = R) &@L), 1, 1]},
      Return[set[[1]], Set] /; set =!= {}
    ]
  ]

This surely adds significant and possibly unacceptable overhead. Nevertheless:

obj = Obj[1];

obj["Property"] = 256;

properties
{256}

I make no claim that this is in any way robust. Perhaps something better will come to me later.

$\endgroup$
1
  • $\begingroup$ Congrats on hitting 100k! $\endgroup$
    – Artes
    Jul 17, 2014 at 9:34
4
$\begingroup$

How about the old Gayley-Villegas trick?

Obj /: (lhs_ = Obj[id_]) := 
  Block[{$inSet = True},
 lhs /: (lhs["Property"] = value_) := setObjProperty[id, value];
 lhs /: Unset[lhs] := ClearAll[lhs];
 lhs = Obj[id]
  ] /; ! TrueQ[$inSet]

Then we get the following behaviour:

obj = Obj[1];
UpValues[obj]
{HoldPattern[obj["Property"] = value$_] :> setObjProperty[1, value$]}
obj["Property"] = 42;
properties
{42}

It's a bit dirty though, but it might just be what you're after.

$\endgroup$
3
  • $\begingroup$ Interesting trick. But what if I do obj =. next? The up-value stays. $\endgroup$
    – Szabolcs
    Jul 16, 2014 at 22:00
  • $\begingroup$ @Szabolcs Well then we should also overload Unset in the Block ... let's see if I can update the answer. $\endgroup$ Jul 16, 2014 at 22:02
  • $\begingroup$ @Szabolcs This is basically the same idea as what I used here (although not using VG). Didn't suggest it here precisely because of the hanging UpValues problem. +1. $\endgroup$ Jul 16, 2014 at 22:44
1
$\begingroup$

I think the new MutationHandler code is designed for this sort of thing. Here is one implementation:

ClearAll[ObjHandler]
SetAttributes[ObjHandler, HoldAll];

ObjHandler[Set[m_Symbol?objQ[key_String], rhs_]] := (
    Extract[
        m,
        1,
        Function[Null, AssociateTo[#, key->rhs], HoldAll]
    ];
    rhs
)

Language`SetMutationHandler[Obj, ObjHandler]

ClearAll[Obj]
SetAttributes[Obj, HoldFirst]

objQ[m_] := MatchQ[m, _Obj]

(* property extraction *)
Obj /: Obj[a_][key_] := a[key]
Obj /: Obj[a_]["Properties"] := a

(* formatting *)
MakeBoxes[Obj[a_Symbol], StandardForm] ^:= With[{id = Lookup[a, "ID", "<>"]},
    RowBox[{"Obj", "[", MakeBoxes[id], "]"}]
]

(* object creation *)
makeObject[id_] := Module[{z},
    z = <|"ID"->id|>;
    Obj[z]
]

Make an object with id 1:

obj = makeObject[1]

Obj[1]

Set key "property1" to 23, and "property2" to "Real":

obj["property1"] = 23
obj["propery2"] = "Real"

23

"Real"

Find properties of obj:

obj["Properties"]

<|"ID" -> 1, "property1" -> 23, "propery2" -> "Real"|>

"property1":

obj["property1"]

23

InputForm of obj:

obj //InputForm

Obj[z$136716]

Definition of z$133176:

z$133176

<|"ID" -> 1, "property1" -> 23, "propery2" -> "Real"|>

While it is possible to create an auxiliary variable to store definitions, it is probably more robust just to store the information in the object itself. So I recommend an alternate approach:

ClearAll[ObjHandler]
SetAttributes[ObjHandler, HoldAll];

ObjHandler[Set[m_Symbol?objQ[key_String], rhs_]] := With[
    {new = Association[First @ m, key -> rhs]},
    Set[m, Obj[new]];
    rhs
]

Language`SetMutationHandler[Obj, ObjHandler]

ClearAll[Obj]

objQ[m_] := MatchQ[m, _Obj]

(* property extraction *)
Obj /: Obj[a_][key_] := a[key]
Obj /: Obj[a_]["Properties"] := a

(* formatting *)
MakeBoxes[Obj[a_Association], StandardForm] ^:= With[{id = Lookup[a, "ID", "<>"]},
    RowBox[{"Obj", "[", MakeBoxes[id], "]"}]
]

Create an object:

obj = Obj[<|"ID"->2|>]
obj //InputForm

Obj[2]

Obj[<|"ID" -> 2|>]

Assign values:

obj["p1"] = x + 1
obj["p2"] = y

1 + x

y

Check properties:

obj["Properties"]
obj["p1"]

<|"ID" -> 2, "p1" -> 1 + x, "p2" -> y|>

1 + x

Compare StandardForm and InputForm of obj

obj
obj //InputForm

Obj[2]

Obj[<|"ID" -> 2, "p1" -> 1 + x, "p2" -> y|>]

The nice thing about this approach is that all of the property information is embedded in the object, and so it is still there even after restarting the kernel.

$\endgroup$
0
$\begingroup$

UPD: Looks like I accidentally dismissed everything in the post below the horizontal line confusing it to comments, and tried to answered a more general question than needed. Too bad.

To clarify: the idea represented below is to have [still immutable] objects with pointers (in the form of symbols) to their properties inside the objects themselves, which makes it possible to extract pointers from expression anytime if needed.

For ease of use, however, I propose a way to define the pointers' structure unambiguously by the object structure; and instead of just placing pointers inside Hold, wrap them in Dynamic for a cleaner representation in the FrontEnd. In effect, properties become usual settable expressions.


It is possible that the answer is that it is simply not a good idea to do this.

It's probably true.

If you want to refer to object with properties as

Obj[id_]["Property"]

and want to Set

obj["Property"] = 137

then why not just

Obj[_]["Property"] := 137

whenever you need it?

From my modest but somewhat extensive experience, the only real reason one might need a flexible general complex mutable structure is for bulding complex graphical interfaces with minimal amount of boilerplate. This is also probably the only moment when one needs something like “object id”.

What I ended up with was looking like this:

(Note: Unevaluated and Holds are probably not needed)

setProperty // ClearAll

SetAttributes[setProperty, HoldFirst];

Options@setProperty = {"HeadsHistory" -> HoldComplete[]};

setProperty[head_[most___, outerProperty_@inner__, rest___]
, HoldComplete[outerProperty_, innerProperties__]
, value_
, OptionsPattern[]] :=
head[most
   , setProperty[outerProperty@inner
     , HoldComplete@innerProperties
     , value
     , "HeadsHistory" ->
       Append["HeadsHistory" // OptionValue
       , Unevaluated@head]]
   , rest]

setProperty[head_[most___, mostInnerProperty_Symbol@Dynamic@expr_, rest___]
, HoldComplete@mostInnerProperty_Symbol
, value_
, OptionsPattern[]] :=
( expr = value;
  head[most, mostInnerProperty@Dynamic@expr, rest] )

setProperty[head_[most___, mostInnerProperty_Symbol[], rest___]
, HoldComplete@mostInnerProperty_Symbol
, value_
, "HeadsHistory" -> HoldComplete@heads__] :=
( mostInnerProperty@Compose[head, heads] = value;
  head[most
     , mostInnerProperty @
       Dynamic@mostInnerProperty@Compose[head, heads]
     , rest] )

How this works:

setProperty[
  "car"[1][
    "engine"[power[], type[]]
  , "driver"[name[], mood[]]]
, HoldComplete["engine", power]
, 250]

car[1][engine[power[500], type[]], driver[name[], mood[]]]

Of course, engine's power is only displayed as 500, by means of Dynamic. The real value is stored in a transparently structured expression:

setProperty[
  "car"[1][
    "engine"[power[], type[]]
  , "driver"[name[], mood[]]]
, HoldComplete["engine", power]
, 500] // FullForm

"car"[1][
  "engine"[power[Dynamic[power[Compose["engine", "car"[1]]]]], type[]]
, "driver"[name[], mood[]]]

Note that we've reset the value to 500.

Call it:

power@"engine"@"car"@1
500

A drawback: you have to use symbols for innermost keys. However, setProperty returns a complete object, with all its properties in it, and it is displayed nicely.

I haven't yet thought about possible applications for 2nd, 3rd, … arguments of Dynamic here.

When one can't afford a symbol for particular property's name, one may stick to strings, and use a universal symbolic head, like Property (Which would make references slightly more cumbersome, of course.), in which case the Dynamic wrapper probably becomes obsolete, unless one uses the technique to build complex interactive GUIs.

$\endgroup$
3
  • $\begingroup$ Regarding why I need an object ID, and why this is non-negotiable: the ID is a handle to a data structure implemented in C, not in Mathematica, and I'm also using managed library expressions. Obj[1] and Obj[2] refer to different data structures. $\endgroup$
    – Szabolcs
    Jul 17, 2014 at 15:47
  • $\begingroup$ @Szabolcs Oh, I didn't notice that in the initial post. :-( My answer seems to be completely useless then. $\endgroup$
    – akater
    Jul 18, 2014 at 2:45
  • $\begingroup$ Well, it's not useless, it's interesting. I just can't apply it to my problem :-) In the end I'm using a syntax like ObjSet[obj, property -> value] to change the state of obj. $\endgroup$
    – Szabolcs
    Jul 18, 2014 at 2:59
0
$\begingroup$

The way I tend to deal with this problem is by using MessageName instead of SubValues and overloading it. By attaching a single or small set of UpValue overrides to MessageName we get a visually appeal way to do OOP that also doesn't add vastly too much overhead. It is, of course, never terribly advisable to mess with built-ins but I think given the specialized nature of MessageName we are likely safe (and in fact I've used this extensively in various projects of mine without issue).

To do this we'll do a Villegas-Gayley:

isObj[s_] := 
  MatchQ[OwnValues[
    s], {Verbatim[HoldPattern][HoldPattern[s]] :> _Obj}];
isObj~SetAttributes~HoldFirst;
Unprotect@MessageName;
MessageName[s_, prop : Except["usage"]] /; ! 
    TrueQ@$messageNameOverride :=
  If[isObj@s,
   getattr[s, prop],
   Block[{$messageNameOverride = True}, MessageName[s, prop]]
   ];
MessageName /: 
  HoldPattern[Set[MessageName[s_, prop : Except["usage"]], v_]] /; ! 
    TrueQ@$messageNameOverride :=
  If[isObj@s,
   setattr[s, prop, v],
   Block[{$messageNameOverride = True}, MessageName[s, prop]]
   ];
Protect@MessageName;

where the isObj is just a safe MatchQ[s,_Obj] and getattr and setattr are get / set functions of your choosing.

And what you get is this:

In[176]:= a = Obj[1]

Out[176]= Obj[1]

In[177]:= a::b

Out[177]= getattr[Obj[1], "b"]

In[178]:= a::b = 2

Out[178]= setattr[Obj[1], "b", 2]

A great thing about this is that since MessageName converts the second argument to a string implicitly one can make private attributes that can't be accessed via this mechanism using Key or something.

This does add significant overhead to MessageName:

In[179]:= Do[a::b, 10000] // AbsoluteTiming // First

Out[179]= 0.056259

In[180]:= Unprotect@MessageName; Clear@MessageName; Protect@MessageName;

In[182]:= Do[a::b, 10000] // AbsoluteTiming // First

Out[182]= 0.002118

But I don't think MessageName is called often enough for this to be entirely disqualifying.

$\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.