Question about circular references
David Alayachew
davidalayachew at gmail.com
Fri Jun 30 22:28:46 UTC 2023
Hello all,
First off, please let me know if I have CC'd the wrong groups. I've CC'd
the Amber Dev Team since this involves records, but it's not specifically
about records.
---
For the past couple of weeks, I have been building a program that uses a
State Transition Diagram (aka State Machine, Finite Automata, etc. --
https://en.wikipedia.org/wiki/Finite-state_machine) to model part of its
control flow. I've been using some Amber features to facilitate this (and
having a wonderful time of it), but then I hit a snag.
Here is a(n EXTREMELY) simplified version of my actual problem. Imagine I
have code like the following.
```java
sealed interface Node<T> permits StartNode, BranchingNode, EndNode
{...unrelated stuff here...}
record StartNode<T> (Node<T> a, Node<T> b, Node<T> c) implements Node<T> {}
record BranchingNode<T> (Node<T> a, Node<T> b, Node<T> c, ...fields
unrelated to transitioning...) implements Node<T> {}
record EndNode<T> (...fields unrelated to transitioning...) implements
Node<T> {}
```
This type hierarchy is meant to represent a control flow of sorts. Control
flow is (imo) best modeled using a State Transition Diagram, so I
instinctively reached for that. And since my API needed to be nothing but
the data (each Node needed to be tightly coupled to my internal state
representation), I realized that this is an ideal use case for records.
Things worked out well enough until I tried to model a circular
relationship.
Through chance, all of my control flows up to this point were tree-like, so
I could model them by starting from the "leaves," then climbing up until I
got to the "roots". To use State Transition Diagram terminology, I started
from my exit states and modeled my way up to my entry states.
For example, assume that my State Transition Diagram is as so.
S ---a---> T
S ---b---> U
S ---c---> V
T ---a---> U
T ---b---> V
T ---c---> E
U ---a---> V
U --b|c--> E
V -a|b|c-> E
S is my StartNode, and E is my ExitNode.
In this case, modeling with records is easy. It would look like so.
```java
ExitNode<UnrelatedStuff> e = new ExitNode<>(...unrelated...);
BranchingNode<UnrelatedStuff> v = new BranchingNode<>(e, e, e,
...unrelated...);
BranchingNode<UnrelatedStuff> u = new BranchingNode<>(v, e, e,
...unrelated...);
BranchingNode<UnrelatedStuff> t = new BranchingNode<>(u, v, e,
...unrelated...);
StartNode<UnrelatedStuff> s = new StartNode<>(t, u, v);
return s;
```
But once I hit a circular reference, I could not figure out how to model
the code using the same format.
For example, what if I say the following instead?
V ---a---> T
How do I model that using my current representation?
Obviously, I could change my representation, but all of them required me to
"taint" my representation in incorrect ways.
For example, I could swap out my records for simple classes where the
references to Node's were mutable. But I strongly disapprove of this
strategy because these nodes do NOT have a mutable relationship. Obviously,
I could put something in the Javadoc, but I want to fix the incorrect
representation, not put a warning sign in front of it.
Also, I could use indirection, like having a separate Map whose values are
the actual Node references and the keys would be a record Pair<T>(String
nodeId, Branch branch) {} where Branch is enum Branch { a, b, c, ; } and
then give each Node an id, changing my record to now be record
BranchingNode<T> (String id, ...the same as above...) {}. But ignoring the
fact that I now have to deal with managing an id, I've also added a lot of
unnecessary bloat and indirection just to get the circular reference I
wanted. What should be a direct relationship now requires a Map lookup.
In that same vein, someone suggested that I use pattern-matching for
switch, but that would require me to create a new switch expression for
every single state. That's even more verbose and indirect than the Map. At
least with the map, I can put them all in one expression. This strategy has
an expression for each state!
I've been told that there is another pathway involving reflection, but it
basically amounts to breaking the rules of Java. Apparently, you can turn
off finality to insert in fields you want, and then turn it back on? I
liked this idea the least compared to all of the others, so I didn't pursue
it any further.
In the end, I decided to go down the Map lookup route. But I just wanted to
point out my experience with this because it was a surprising and annoying
speed bump along an otherwise smooth road. I didn't think that something as
small as a circular reference would require me to uproot my entire solution.
And finally, I want to emphasize that the same blockers above apply no
matter what pathway I go down. I had actually tried implementing this first
as an enum before I tried a record, since an enum would more accurately
represent my state.
```java
enum State
{
V(T, EXIT, EXIT), //FAILURE -- T cannot be referenced yet
U(V, EXIT, EXIT),
T(U, V, EXIT),
;
...public final fields and constructor...
}
```
But even then, the same problem occurred -- I can't reference an enum value
until it has been declared. I thought going down the path of records would
give me the flexibility I wanted, but no dice.
It reminded me of that one programming meme.
> * High Quality
> * Quickly Built
> * Low Cost
>
> You can only pick 2
But instead, it's
* Circular Relationship
* Immutability
* Direct References
What are your thoughts? Is this a problem in your eyes too? Or simply a
non-issue?
Thank you for your time and insight!
David Alayachew
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20230630/a87dd966/attachment.htm>
More information about the amber-dev
mailing list