# How to define “typed” objects and their operator specializations?

I've defined a primitive sort of type system where my objects are defined as lists with an associated identifier:

{scalarType, vectorType, bivectorType, trivectorType, multivectorType} = Range[5];
v = { vectorType, 3 PauliMatrix[2] }
b = { bivectorType, Sin[x] PauliMatrix[2] PauliMatrix[1] }
t = { trivectorType, I IdentityMatrix[2] }
m = { multivectorType, IdentityMatrix[2] + PauliMatrix[3] }


I can use these to specialize various operations. For example

directProduct[ t_, v1_, v2_ ] := { t, (v1 // Last). (v2 // Last) } ;
signedSymmetric[ t_, v1_, v2_, s_ ] := Module[ {a = (v1 // Last), b = (v2 // Last)}, {scalarType, (a . b + s b . a)/2} ] ;
symmetric[ t_, v1_, v2_ ] := signedSymmetric[ t, v1, v2, 1 ] ;
antisymmetric[ t_, v1_, v2_ ] := signedSymmetric[ t, v1, v2, -1 ] ;

ClearAll[ pScalarQ, pVectorQ, pBivectorQ, pTrivectorQ ]
pScalarQ[m : {scalarType, _}] := True ;
pScalarQ[m : {_Integer, _}] := False ;
pVectorQ[m : {vectorType, _}] := True ;
pVectorQ[m : {_Integer, _}] := False ;
...

Scalar[v_] := {scalarType, v IdentityMatrix[2]};
Vector[v_, k_Integer /; k > 0 && k < 4] := {vectorType, v PauliMatrix[k] };
Bivector[v_, k_Integer /; k > 0 && k < 4, j_Integer  /; j > 0 && j < 4] := {bivectorType, v PauliMatrix[k] . PauliMatrix[j] };
Trivector[v_] := {trivectorType, v I IdentityMatrix[2]};

DotProduct[ v1_?pVectorQ, v2_?pVectorQ]    :=     symmetric[ scalarType,   v1, v2 ] ;
DotProduct[ v1_?pVectorQ, v2_?pBivectorQ]  := antisymmetric[ vectorType,   v1, v2 ] ;
...

WedgeProduct[ v1_?pVectorQ, v2_?pVectorQ ]    := antisymmetric[ bivectorType,  v1, v2 ] ;
WedgeProduct[ v1_?pVectorQ, v2_?pBivectorQ ]  :=     symmetric[ trivectorType, v1, v2 ] ;
WedgeProduct[ v1_?pVectorQ, v2_?pTrivectorQ ] := Scalar[0] ;

GeometricProduct[ v1_, v2_ ] := directProduct[ multivectorType, v1, v2 ] ;


I like to do a few things:

0) Be able to distinguish my types from others in a robust way. For example, Sin[x] vs. my ad-hoc vector type {vectorType, ...}

1) Be able to multiply these objects by expressions, and extend Times in a natural way. For example, if that expression is not one of these types, then I would want the multiplication just apply to the second part, whereas Times for of object types such a vector, bivector would change the type to multivector, respect the multiplication order of the objects being multiplied, and multiply all the second parts of the objects respectively. Example:

Cos[x] * v
(v)(b)
> { vectorType, 3 Sin[x]Cos[x] PauliMatrix[2] }
> { multivectorType, 3 Sin[x] PauliMatrix[2] PauliMatrix[2] PauliMatrix[1] }


[EDIT: J.D suggests that I want to be customizing NonCommutativeMultiply instead of times, but doing so takes me back to problem (0). Also, I actually want object specific commutation selection: NonCommutativeMultiply when the objects have type vector or bivector or multivector, but Times when one of the factors is scalar or trivector].

2) I'd like to be able to implement a custom Plus function that behaves according to the underlying types. i.e. if the two objects match then the second part gets added, but if they are different types, the new result is a multivector (with the second parts also added). I have such an addition function, but it would be nicer if I did not have to call it explicitly.

3) Have the output of the result in the notebook look different than the internal representation. I think this amounts to overriding DisplayForm for each of my types, but I am not sure how to do that.

Finally, I suspect this is not a natural way to implement operators on a set of typed objects in Mathematica. If there is a better approach, I'd be interested in some tips for more effective strategies (an unfortunately open ended question).

• Times[] would be appropriate if multiplication of those objects was commutative (since Times[] has the Orderless attribute); you might consider using NonCommutativeMultiply[] if multiplication isn't commutative. – J. M. is away Dec 23 '16 at 14:02
• I would use custom heads and create UpValues for those heads. A simple example is to denote scalars as a = scalar[3], and "overload" addition as scalar /: scalar[a_] + scalar[b_] := scalar[a+b]. Then checking the "type" amounts to checking the Head of stuff. – Marius Ladegård Meyer Dec 23 '16 at 15:34

Marius's comment provided the direction required. Use of an upvalue gives a named type to the objects in question, allowing them to be distinguished from numeric or expression values, and customize all the operations accordingly. Example:

Scalar[ v_ ] := grade[ 0, v IdentityMatrix[ 2 ] ] ;
Vector[ v_, k_Integer /; k == 1 ] := grade[ 1, v PauliMatrix[ 1 ] ] ;
Vector[ v_, k_Integer /; k == 2 ] := grade[ 1, v PauliMatrix[ 3 ] ] ;
Bivector[ v_ ] := grade[ 2, v PauliMatrix[ 1 ] . PauliMatrix[ 3 ] ] ;

(* Times[ -1, _ ] *)

(* Times *)

(* NonCommutativeMultiply *)
...


This resolved parts 0,1,2 of my question, leaving only (3) unresolved: how to make the default display of such an object more natural without explicit use of a modified like // TraditionalForm.

• You can use Format to affect the default display. Simple example: Format[ foo[a_, b_] ] := Subscript[a, b] – jjc385 Mar 31 '17 at 23:52
• Thanks @jjc385. That works nicely. – Peeter Joot Apr 20 '17 at 22:36