I decided it was worth giving another example of modern OOP in Mathematica. There will be a small amount of code, but almost all of it is boiler-plate. I use a package to handle most of the boiler plater, myself, but I find that links to packages make the code less-likely to be used.
So here we go.
First off, we'll define a constructor for your Algebra
object. We'll store all the core data for it in an Association
for convenience of access. You'll take the passed in arguments and turn them into an Association
in canonicalizeAlgebraData
ClearAll[Algebra];
Options[Algebra] = {
"MultTable" -> None,
"Generators" -> None,
"Dimension" -> None
};
Algebra[ops : OptionsPattern[]] :=
Algebra[canonicalizeAlgebraData[ops]];
(* do the minimum work necessary to make sure all the data for the Algebra is there *)
canonicalizeAlgebraData[ops : OptionsPattern[]] :=
Association[ops];
Next we'll add a validator to the object so that we can check if something with Algebra
as its Head
is truly a proper algebra object. I use System`Private`SetValid
to set a flag so that Mathematica doesn't check this again and again. I also provide a AlgebraQ
and a NotAlgebraQ
just for convenience.
(* make some validators so you can always be sure you have a valid algebra without constantly having to check it *)
validateAlgebraData[a_Association] :=
Length[a]>0; (* reimplement this *)
Algebra[a_Association]?NotAlgebraQ :=
System`Private`HoldSetValid[Algebra[a]] /; validateAlgebraData[a];
AlgebraQ[a_Algebra] := System`Private`HoldValidQ[a];
AlgebraQ[_] := False;
AlgebraQ[s_Symbol] := (Head[s] === Algebra && AlgebraQ[Evaluate[s]]);
AlgebraQ~SetAttributes~HoldFirst;
NotAlgebraQ[a_] := Not[AlgebraQ[a]];
NotAlgebraQ~SetAttributes~HoldFirst;
Next we can supply a formatting rule so that it looks nice when displayed. I use ArrangeSummaryBox
for that.
(* define formatting if you want to *)
Format[Algebra[a_]?AlgebraQ, StandardForm] :=
RawBoxes@
BoxForm`ArrangeSummaryBox[
Algebra,
Algebra[a],
None,
{"Put summary info here"},
{},
StandardForm
]
Finally we get to the fun stuff. First we'll make it so that you can treat the Algebra
like an Association
by defining a standard accessor. Then I have two examples of how you can delegate accessors for things that should be computed (like the "Generators"
) to specialized functions.
(* define some accessors / methods on your alebgra *)
Algebra[a_]?AlgebraQ[k_] :=
Lookup[a, k];(* general lookup *)
(g : Algebra[a_]?AlgebraQ)["Generators"] :=
getAlgebraicGenerators[g];
(g : Algebra[a_]?AlgebraQ)["Dimensions"] := getAlgebraDimension[g];
Now we get the part where we hook this into the functions Mathematica already has in it. All of this is done with TagSet
. First is the Dimensions
, because it's a pretty obvious analog to alg["Generators"]
.
The next one is more subtle. I first overload Part
so that it wraps the requested part in the head AlgebraicElement
. I do this so that you can then define an overload for NonCommutativeMultiply
with this head. That means that now you can do, e.g. alg[[1]] ** alg[[10]]
and it'll turn into getAlgebraProduct[alg, {1, 10}]
. I'm not sure if it makes sense to multiply elements from different algebras, so I didn't support that.
You can play with this however you like.
(* define some overloads for your algebra *)
Algebra /: Dimensions[a_Algebra?AlgebraQ] := a["Dimensions"];
Algebra /: a_Algebra?AlgebraQ[[el_]] :=
AlgebraicElement[a, el];(* getting elements *)
AlgebraicElement /:
NonCommutativeMultiply[
AlgebraicElement[a_Algebra?AlgebraQ, el1_],
AlgebraicElement[a_Algebra?AlgebraQ, el2_]
] := getAlgebraProduct[a, {el1, el2}];
You can also add support for "mutation" of your structure, that is allowing the object to be affected by stuff like AssociateTo[alg, someRule]
. I do this via a simple function that merges the changes into the structure (mutateAlgebra
) and then I create a second function that'll hook this into Mathematica's assignment system for Symbol
using Language`SetMutationHandler
on a handler function (algebraMutationHandler
). This part is a bit subtle, but really powerful and can be very useful.
(* allow for natural modifications of the algebraic structure *)
mutateAlgebra[Algebra[a_]?AlgebraQ, changes_Association] :=
Algebra[Join[changes, a]];
mutateAlgebra[a_Algebra, changes_] :=
mutateAlgebra[a, Association@changes]
algebraMutationHandler~SetAttributes~HoldAllComplete;
algebraMutationHandler[
AssociateTo[s_Symbol?AlgebraQ, stuff_]
] :=
(s = mutateAlgebra[s, stuff]);
algebraMutationHandler[
Set[s_Symbol?AlgebraQ[key_], val_]
] :=
(s = mutateAlgebra[s, key -> val]);
Language`SetMutationHandler[Algebra, algebraMutationHandler];
Finally this is the part that you have to do: actually implementing the functions that do algebraic calculations on one of these objects.
(* implement the core algebra calculations some other way *)
getAlgebraGenerators[a_Algebra?AlgebraQ] :=
...;
getAlgebraDimension[a_Algebra?AlgebraQ] :=
...;
getAlgebraProduct[a_Algebra?AlgebraQ, {el1_, el2_}] :=
...;
Here's an example of how this all works:
a = Algebra["MultTable" -> {}]
(* Out: Algebra[<|"MultTable" -> {}|>] *)
Since I added those formatting rules, this looks like:

Then we can mutate this thing, say to add a rule:
a["Gens"] = 1;
a["Gens"]
(* Out: 1 *)
a
(* Out: Algebra[<|"Gens" -> 1, "MultTable" -> {}|>] *)
And if we ask for those overloads we get the desired results:
Dimensions[a]
(* Out: getAlgebraDimension[Algebra[<|"Gens" -> 1, "MultTable" -> {}]|>] *)
a[[1]] ** a[[10]]
(* Out: getAlgebraProduct[Algebra[<|"Gens" -> 1, "MultTable" -> {}|>], {1, 10}] *)
Hopefully this has been a nice little example of how a specific case of OOP can work. Also you can probably see how most of that boiler plate (defining a validator, constructor, mutation handlers, etc.) can be abstracted away into a package. I create a lot of objects like this, using a package I called InterfaceObjects
since they define interfaces to Association
data.