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:
- All top-level
Temporary
allows forAutomatic
garbage collection- By using the
Association
interface we can get efficient object emulation System`Private`SetNoEntry
allows our object to act atomicLanguage`SetMutationHandler
fixes the issue of how to set parts on the object whenUpValues
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[]
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"}]
Then we can instantiate the iter:
inst = iter["New"][Range[5]]
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
MSE discussion "Which Object-oriented paradigm approach to use in Mathematica?".
Blog post "Object-Oriented Design Patterns in Mathematica".
Blog post "UML diagrams creation and generation".
This answer in the discussion General strategies to write big code in Mathematica?.
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.