How can I implement object oriented programming in Mathematica?

Here's I think my 5th version of this type of thing. I've got another framework for this here whose construction I explained here.

The package version of this lives on GitHub, which you can load by:

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

Why do this again

OOP is overdone (well, over-emulated) in Mathematica. Personally I've done this upwards of 5 times in some capacity, each with different levels of sophistication and memory footprint.

This time I wanted to have a very light-weight, robust implementation that doesn't affect any part of the system at all.

Basic Setup

Core idea

The driving principle behind this was to effectively make my objects Associations (hash maps), but encapsulated in a mutable Symbol with accessors and things to make them really feel like an Association.

To do this I picked a primary Head to wrap my Symbols which I called SObj and it was pretty much straightforward from there.

I'm going to omit parts of this that don't really give us much. Follow along by looking at the package if you want the entire story. It's ~500 LOC and I don't want to copy the entire text here.

Constructing a new object

The first thing I needed to do was build a constructor function, which I called SObjNew:

SObjNew[
   name : _String | Automatic : Automatic,
   templates : {___String} : {},
   a : _Association : <||>
   ] :=
  With[
   {
    symbol =
     Unique[
      c <> "$Objects`" <>
       If[StringQ@name, name, "SObj"] <>

           "$"
      ],
    state =
     Merge[
      {
       "ObjectID" -> CreateUUID["sobj-"],
       a,
       Lookup[$SObjTemplates,
        Prepend[
         templates,
         If[StringQ@name, name, "Object"]
         ], 
        {}
        ],
       "ObjectType" -> 
        If[StringQ@name, name, "Object"],
       "ObjectTemplates" -> templates
       },
      First
      ]
    },
   SetAttributes[symbol, Temporary];
   symbol = state;
   SObj[symbol]
   ];

The main things to note here: we use Unique to build a new Symbol in a specific context. We give that Symbol the attribute Temporary so it can automatically be garbage collected.

There's also stuff for managing templates (essentially classes, but with a different sort of inheritance mechanism so I didn't want to call them that).

Defining accessor functions

Since we want this to work like an Association we need to support a bunch of different types of attribute lookups. Primarily we'll support [], [[]], Extract, and Lookup. These give us the broadest coverage:

(*
SObjAccess[___]:
Basic [___] wrapper for SObj
*)
SObjAccess[
  o : SObj[s_Symbol],
  k__
  ] :=
 If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
  s[k]
(*
SObjPart[___]:
Part wrapper for SObj
*)
SObjPart // Clear
SObjPart[
   o : SObj[s_Symbol], 
   k : _Span | _List
   ] :=
  If[$SObjGetWrap, SObj, Identity]@Evaluate@
    s[[k]];
SObjPart[
   o : SObj[s_Symbol], 
   k__
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   s[[k]];
(*
SObjExtract[___]:
Extract wrapper for SObj
*)
SObjExtract // Clear
SObjExtract[
   o : SObj[s_Symbol], 
   k : {_List} | Except[_List]
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Extract[s, k];
SObjExtract[
   o : SObj[s_Symbol], 
   k_
   ] :=
  If[$SObjGetWrap, SObj, Identity]@Evaluate@
    Extract[s, k];
SObjExtract[
   o : SObj[s_Symbol], 
   k_,
   h_
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Extract[s, k, h];
(*
SObjLookup[___]:
Lookup wrapper for SObj
*)
SObjLookup[
   o : SObj[s_Symbol], 
   k_
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Lookup[s, k];
SObjLookup[
   o : SObj[s_Symbol], 
   k_,
   d_
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Lookup[s, k, d];
SObjLookup[
   o : {__SObj},
   k_
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Lookup[SObjSymbol /@ o, k];
SObjLookup[
   o : {__SObj},
   k_,
   d_
   ] :=
  If[$SObjGetDecorate, SObjGetDecorate[o, #] &, Identity]@
   Lookup[SObjSymbol /@ o, k, d];

You'll notice two weird parameters here, $SObjGetDecorate and $SObjGetWrap. The first of these handles a special type of attribute I called an SObjMethod. SObjGetDecorate automatically binds these to the instance they were extracted from. It's a convenience, but can be inefficient, so we allow it to be turned off. The latter handles the fact that we want these to act like Association and when you do <|1->2, 2->3, 3->4|>[[2;;]] you get an Association back. By analogy we should expect to get an SObj out when we do obj[[2;;]].

Defining setter functions

Association supports a few different setting methods, a[___]=val, a[[__]]=val, and AssociateTo[a, rules]. We want to support all of them for flexibility. Happily this is pretty easy as we just delegate to the held Symbol:

(*
SObjSet[___]:
Set wrapper for SObj
*)
SObjSet[SObj[s_], prop_, val_] :=
  s[prop] = val;
(*
SObjSetPart[___]:
Set Part wrapper for SObj
*)
SObjSetPart[SObj[s_], part__, val_] :=
  s[[part]] = val;
(*
SObjAssociateTo[___]:
AssociateTo wrapper for SObj
*)
SObjAssociateTo[SObj[s_], parts_] :=
  (AssociateTo[s, parts];);

And with this we've defined the core of our lower-level interface.

Setting up a good object type

This is where the bulk of the fun starts. We'll want our SObj object to delegate to all those methods we just defined in the right cases.

Constructor

First we define a constructor case:

SObj[
   name : _String | Automatic : Automatic,
   templates : {___String} : {},
   a : _Association : <||>
   ] :=
  SObjNew[name, templates, a];

NoEntry

Then we'll do a funky thing, since we're going to want our SObj to look atomic, we'll make sure that if it's a valid object it's got System`Private`NoEntryQ.

o : SObj[s_Symbol] /; (
   System`Private`EntryQ[Unevaluated@o] &&
    SObjSymbolQ[s]
   ) :=
 (System`Private`SetNoEntry[Unevaluated@o]; o)

Property access

Then we define our property access, mostly via UpValues:

(o : SObj[s_Symbol]?SObjQ)["Properties"] :=
  SObjKeys@o;
(o : SObj[s_Symbol]?SObjQ)[
   i__
   ] :=
  (SObjAccess[o, i]);
SObj /: Lookup[SObj[s_Symbol]?SObjQ, i__] :=
  (SObjLookup[s, i]);
SObj /: Part[o : SObj[s_], p___] :=
  (SObjPart[o, p] /; SObjQ@o);
SObj /: Extract[o : SObj[s_], 
   e___] :=
  (SObjExtract[o, e] /; SObjQ@o);

Property setting

Here's where most of my old attempts have really struggled. It's easy to redefine things like a / : Set[a[p_], b_]:=(mySet[a, p, b]), but it's harder to handle Part and friends without things getting messy. Happily we now have the function Language`SetMutationHandler which will do this for us.

We'll want to handle all the cases we declared before, so we set it up like this:

SetAttributes[SObjMutationHandler, HoldAllComplete];
SObj /: Set[o_SObj?SObjQ[prop_], newvalue_] :=

  SObjSet[o, prop, newvalue];
SObjMutationHandler[
   Set[(sym : (_SObj | _Symbol)?SObjQ)[prop_], newvalue_]
   ] := SObjSet[sym, prop, newvalue];
SObjMutationHandler[
   Set[Part[(sym : (_SObj | _Symbol)?SObjQ), p__], newvalue_]
   ] := SObjSetPart[sym, p, newvalue];
SObjMutationHandler[
   AssociateTo[(sym : (_SObj | _Symbol)?SObjQ), stuff_]
   ] := SObjAssociateTo[sym, stuff];
Language`SetMutationHandler[SObj, SObjMutationHandler];

A brief note on MutationHandlers

Note that without a mutation handler, there are a few core cases that would be very tough. For instance, if we wanted to set a part on an object and tried it naively:

a = obj[uuid];
a[[1]] = 2;
a

obj[2]

it doesn't work.

Similarly, if we tried to set a part on an Association we set as a SubValue:

b = obj[uuid];
b[1] = <||>;
b[1, 2] = 10;
b[1]

<||>

this also fails

Alternatively, if we use a MutationHandler in a minimal object implementation:

myO`myO~SetAttributes~HoldFirst;
myO`myO[s_Symbol][k__] := s[k];
myO`myO[[s_Symbol]][k__] := s[[k]];
myO`setPart[myO`myO[s_Symbol], p__, v_] :=
  s[[p]] = v;
myO`setKey[myO`myO[s_Symbol], p__, v_] :=
  s[p] = v;
myO`mutate~SetAttributes~HoldAllComplete;
myO`mutate[a_Symbol?(Head[#] === myO`myO &)[k__]~Set~ v_] :=

  myO`setKey[a, k, v];
myO`mutate[a_Symbol?(Head[#] === myO`myO &)[[k__]]~Set~ v_] :=

  myO`setPart[a, k, v];
Language`SetMutationHandler[myO`myO, myO`mutate];

We can handle those cases:

o = myO`myO[Evaluate@Unique[]];
o[1]
o[1] = 2

$1406[1]

2

o =
  With[{s = Unique[]},
    s = Range[10];
   myO`myO[s]
   ];
o[[1]] = 2;
First@o

{2, 2, 3, 4, 5, 6, 7, 8, 9, 10}

Attributes

Finally we'll need to make this thing HoldFirst:

SObj~SetAttributes~HoldFirst

Take-aways

Hopefully that was a simple enough explanation of one of many takes on OOP in Mathematica. The key things that this provides for us:

  1. All top-level
  2. Temporary allows for Automatic garbage collection
  3. By using the Association interface we can get efficient object emulation
  4. System`Private`SetNoEntry allows our object to act atomic
  5. Language`SetMutationHandler fixes the issue of how to set parts on the object when UpValues will only go to depth 1 as well as the more crucial issue of how to handle mutating the object if it's bound to a symbol.

Classes

I added in an instantiation / class system to this based on simply having an object with a "New" method that will call a function SObjInstantiate that will build an instance from the object.

Similar features are simple enough building off that.


Examples

Basic mutability

Here's a little example of how this works:

Get["https://github.com/b3m2a1/mathematica-tools/raw/master/SymbolObjects.wl"];
myObj = SObj[]

asd

AssociateTo[myObj, 
 KeyMap[CanonicalName]@ChemicalData["Water", "Association"]
 ]

myObj // Length

113

myObj["BondLengths"]

{Quantity[1., "Angstroms"]}

myObj["BondLengths"] *= 2

myObj["BondLengths"]

{Quantity[1.9, "Angstroms"]}

myObj[["BondLengths", 1, 2]] = "Kilojoules"

"Kilojoules"

myObj["BondLengths"]

{Quantity[1.9, "Kilojoules"]}

We can see it generally does act like an Association (although Lookup of course doesn't really work for it), but this object is stateful, is never copied, and when we use it with another Symbol the changes map back:

asd = myObj;

asd["BondLengths"] *= 2

myObj["BondLengths"]

{Quantity[3.8, "Kilojoules"]}

Implementing an iterator

Here's how we can build an iter type with this. First we set up a Next and Skip functions that would work with a passed Symbol capturing an Association:

iterNext[iter_, Optional[1, 1]] :=
  Quiet[
   Check[
    (iter["Index"] += 1; #) &@
     iter[["Iterable", iter["Index"]]],
     EndOfBuffer,
    {Part::partw}
    ],
   {Part::partw}
   ];
iterNext[iter_, n_Integer] :=

  Block[{tally = 0, i = iter["Index"], res, range},
   range =
    i +
     If[n > 0,
      Range[0, n - 1], 
      Range[-1, n, -1]
      ];
   res =
    Join[
     Map[
      Quiet[
        Check[
         iter[["Iterable", #]],
         tally++;
          EndOfBuffer,
         {Part::partw}
         ],
        {Part::partw}
        ] &,
      Select[range, Positive]
      ],
     ConstantArray[EndOfBuffer, Length@Select[range, Not@*Positive]]
     ];
   iter["Index"] = Max@{i + n - tally, 1};
   res
   ];
iterSkip[iter_, n : _Integer : 
    1] :=
  (iter["Index"] = Max@{iter["Index"] + n, 1};);
iterExtend[iter_, l_] :=
  With[{i = Join[iter["Iterable"], l]},
   iter["Iterable"] = i;
   ];

Then we define a function for initializing the iter and a class template which we bind to the $SObjTemplates variable provided by the package.

iterInit[obj_, iter_: {}] :=
  (
   AssociateTo[
    obj,
    {
     "Iterable" -> iter,
     "Index" -> 1
     }
    ];
   obj
   );
SymbolObjects`Package`$SObjTemplates["Iterator"] =
  <|
   "ObjectInstanceProperties" ->
    <|
     "Next" ->
      SymbolObjects`Package`SObjMethod[iterNext],
     "Skip" ->
      SymbolObjects`Package`SObjMethod[iterSkip],
     "Extend" ->
      SymbolObjects`Package`SObjMethod[iterExtend],
     "ExhaustedQ" ->
      SymbolObjects`Package`SObjProperty[
       With[{res = iterNext[#] === EndOfBuffer},
         iterNext[#, -1];
         res
         ] &
       ]
     |>,
   "ObjectInitialize" -> iterInit
   |>;

And now we can initialize our "Iterator" class:

iter = SObj["Iterator", {"Class"}]

asdasd

Then we can instantiate the iter:

inst = iter["New"][Range[5]]

asddd

inst["Next"][]

1

inst["Skip"][]

inst["Next"][5]

{3, 4, 5, EndOfBuffer, EndOfBuffer}

inst["Skip"][-5];
inst["Next"][5]

{1, 2, 3, 4, 5}

inst["Extend"][Range[10]];
inst["Skip"][5]
inst["Next"][10]

{6, 7, 8, 9, 10, EndOfBuffer, EndOfBuffer, EndOfBuffer, EndOfBuffer, EndOfBuffer}

inst["ExhaustedQ"]

True

Final notes

If this interests you, look at the package, email me with questions. If there are things you want to see in the package, let me know and I'll try to get to them when I have time. I really just knocked this thing out as fast as possible (maybe like ~2 or 3 hours) so it's undoubtedly highly incomplete.


[...] but I'm hoping someone can suggest a sleek and novel implementation that is easy to use.

The answer to this is to use Object-Oriented Design Patterns in Mathematica as explained and exemplified in the presentation "Object Oriented Design Patterns" at the Wolfram Technology Conference 2015. (The presentation recording is also uploaded at YouTube.)

Here is a link to a document describing how to implement OOP Design Patterns in Mathematica:

"Implementation of Object-Oriented Programming Design Patterns in Mathematica"

The described approach does not require the use of preliminary implementations, packages, or extra code.

Design Patterns brought OOP into maturity. Design Patterns help overcome limitations of programming languages, give higher level abstractions for program design, and provide design transformation guidance. Because of this it is much better to emulate OOP in Mathematica through Design Patterns than through emulation of OOP objects. (The latter is done in all other approaches and projects I have seen.)

Related posts/descriptions/answers

  1. MSE discussion "Which Object-oriented paradigm approach to use in Mathematica?".

  2. Blog post "Object-Oriented Design Patterns in Mathematica".

  3. Blog post "UML diagrams creation and generation".

  4. This answer in the discussion General strategies to write big code in Mathematica?.

  5. This answer in the discussion Can one identify the design patterns of Mathematica?.


Well, one obvious idea would be to build on the struct implementation by Bob Beretta. You would have to add information about methods and modify the implementation of --> to consider those as well, and for polymorphism, you'd also have to store the base class (or base classes, if multiple inheritance should be supported), and have --> look there if the field or method is not found.