This question appears impossibly long so apologies in advance (skip to the bottom if you want to see the actual questions).
I want to be able to mock Java Classes in Mathematica code. For example, I may have a simple Java bean Point
that has two fields x
and y
. Using JLink, I would interact with objects of the point class as:
obj = JavaNew["java.awt.Point"]
obj@x = 22
obj@y = 42
There are countless ways that this could be done in Mathematica but to mock (so I can later swap out the Mathematica code with the actual Java classes) I'd like to be able to do something along the lines of
obj = Point[0, 0]
obj@x = 22
obj@y = 42
obj == Point[22, 42] (* evals to True*)
I have taken two stabs at this. First using the Notation package and overriding the following type of expressions:
Notation[
ParsedBoxWrapper[
RowBox[{RowBox[{"obj_", "@", "field_"}], "=", "val_"}]] [DoubleLongLeftRightArrow]
ParsedBoxWrapper[
RowBox[{"ApplySet", "[", RowBox[{"obj_", ",", " ", "field_", ",", "val_"}], "]"}]]
The above maps code like
obj_@field_ = val
obj@x = -99 (* for example *)
to
ApplySet[obj_, field_, val_]
ApplySet[obj, x, -99] (* for example *)
ApplySet
in turn is redirected to either a setter on the "class" or passed through to the usual Mathematica handling on the basis of a conditional evaluation that checks that obj
is a "class" or not.
ApplySet[obj_, field_, val_] := set[obj, obj, field, val] /; MBeanQ[Head[obj]]
(* pass obj through twice, once in HoldFirst the second evaluated as the data *)
(* otherwise just pass through to normal handling for the Prefix @ *)
ApplySet[obj_, field_, val_] := obj[field] = val
This approach works well in the FrontEnd and it seemed to be able to do everything I needed and more. However, when I moved this to packages the Notation
and related MakeExpression
/MakeBoxes
doesn't seem to work. I did use Action -> PrintNotationRules
to get the the underlying NotationMakeExpressions
/NotationMakeBoxes
. For example, here is the NotationMakeExpressions
:
NotationMakeExpression[
RowBox[{Notation`Private`lhs___,
RowBox[{obj_, "@", field_}], "=", val_,
Notation`Private`rhs___}], StandardForm] :=
MakeExpression[RowBox[{Notation`Private`lhs,
RowBox[{"ApplySet", "[",
RowBox[{obj, ",", field, ",", val}], "]"}],
Notation`Private`rhs}], StandardForm]
My second crack was to override the behaviour of Set
and redirect in a similar way, testing for a "class". For example:
Set[obj_[field_], val_] := CallSet[obj, obj, field, Null, val] /; CallSetQ[Head[obj]];
Set[obj_[field_[idx_]], val_] := CallSet[obj, obj, field, idx, val] /; CallSetQ[Head[obj]];
Set[obj_, fieldUpdates__List] := CallSet[obj, obj, fieldUpdates] /; CallSetQ[Head[obj]];
This approach works well enough in both the FrontEnd and in packages, but of course it requires overriding something pretty fundamental. Even setting aside the potential performance concerns, it just doesn't seem right.
I'm sure that this is documentation somewhere but I couldn't find it.
Are MakeExpression
and MakeBoxes
really Frontend functions that aren't available in kernel to packages? Is there a smarter way to do this?