<div dir="ltr"><div style="font-family:monospace" class="gmail_default">Hello Ron,<br><br>Thank you for your response!<br><br>> > All of that is to say this. This is a problem that I <br>> > pay a tax for several times a day, every day. So in my<br>> > eyes, this is a very large thorn in my side.<br>> <br>> What is the tax, though? Is it the lack of access to<br>> pattern matching? Is it having to manually write accessor<br>> methods? Is it just boilerplate or is there something<br>> that’s difficult to do correctly without language support?<br><br>In short, it's the boilerplate and indirection combined.<br><br>Pattern-matching is not a problem at all. Pattern-matching actually tends to pair well with STD's.<br><br>But back to the problem -- it seems small at first glance, but it adds up quickly. Let me show you what I mean.<br><br>Starting from literal ground zero, let's pick the simplest State Transition Diagram that has a circular reference -- Rock, Paper, Scissors.<br><br>Since my it's literally just 3 states that have a reference to the other state that they "defeat", then an enum sounds like a perfect fit for this type of problem.<br><br>And btw, here is a stackoverflow post I made about a very similar situation. I encourage reading it and the accepted answer, since it goes into way more depth about the problem I am describing.<br><br><a href="https://stackoverflow.com/questions/75072937/why-does-my-lambda-get-illegal-forward-reference-but-my-anonymous-class-does-no">https://stackoverflow.com/questions/75072937/why-does-my-lambda-get-illegal-forward-reference-but-my-anonymous-class-does-no</a><br><br>enum RPS0<br>{<br><br>       Rock(Paper),<br>  Paper(Scissors),<br>      Scissors(Rock),<br>       ;<br>     <br>      public final losesTo;<br> <br>      RPS0(RPS0 losesTo)<br>    {<br>     <br>              this.losesTo = losesTo;<br>       <br>      }<br><br>}<br><br>Plain, simple, obvious. No confusion or difficulty whatsoever.<br><br>But this doesn't compile -- I am referencing a value that does not yet exist. The specific error I get is illegal forward reference.<br><br>So, I am forced to dig into a couple of other pots. Following your suggestion from earlier Ron, I can do a builder-esque pattern of sorts and go from there.<br><br>So here we go.<br><br>      enum RPS1<br>      {<br>      <br>         Rock,<br>         Paper,<br>         Scissors,<br>         ;<br>      <br>         private RPS1 losesTo;<br>      <br>         static //a static initialization block is the simplest idea imo.<br>         {<br>         <br>            Rock.losesTo = Paper;<br>            Paper.losesTo = Scissors;<br>            Scissors.losesTo = Rock;<br>         <br>         }<br>         <br>         public RPS1 losesTo()<br>         {<br>         <br>            return this.losesTo;<br>         <br>         }<br>      <br>      }<br><br>Now, one could argue that this is actually even slightly simpler to understand than what we saw before. I am willing to concede that. But what happens if we add a new option to the game, let's say Shotgun? We need to make sure not to forget to add the Shotgun branch to our static initialization block. That's a problem.<br><br>Let's try and fix that with a switch expression.<br><br>      enum RPS2<br>      {<br>      <br>         Rock,<br>         Paper,<br>         Scissors,<br>         ;<br>         <br>         public static RPS2 losesTo(RPS2 choice)<br>         {<br>         <br>            return<br>               switch (choice)<br>               {<br>               <br>                  case Rock      -> Paper;<br>                  case Paper     -> Scissors;<br>                  case Scissors  -> Rock;<br>               <br>               };<br>         <br>         }<br>      <br>      }<br><br>Ignore the fact that I didn't include a shotgun, I'm just making a point.<br><br>By using a switch expression, not only have we gained exhaustiveness, which solves our previous problem, but we have actually made our solution a little simpler! I'd go so far as to say that this strategy is better than the hypothetical I proposed.<br><br>Cool, switch expressions are (from what I can see) the best solution for representing single transition STD's.<br><br>But what about multiple transitions? Let's try that with switch expressions.<br><br>      enum RPS3<br>      {<br>      <br>         Rock,<br>         Paper,<br>         Scissors,<br>         ;<br>         <br>         public static RPS3 losesTo(RPS3 choice)<br>         {<br>         <br>            return<br>               switch (choice)<br>               {<br>               <br>                  case Rock      -> Paper;<br>                  case Paper     -> Scissors;<br>                  case Scissors  -> Rock;<br>               <br>               };<br>         <br>         }<br>      <br>         public static RPS3 winsAgainst(RPS3 choice)<br>         {<br>         <br>            return<br>               switch (choice)<br>               {<br>               <br>                  case Rock      -> Scissors;<br>                  case Paper     -> Rock;<br>                  case Scissors  -> Paper;<br>               <br>               };<br>         <br>         }<br>      <br>      }<br><br>So, since switch expressions only can handle one transition, we create another method to handle the other transition.<br><br>But from there, things start to get a little unideal. For starters, now the transitions are separate from each other. This means that if a state has to change significantly, then we need to make sure that we modify all of its transitions. Currently, that is easy enough -- there's only 2 transitions. But you see my point here? If there are 4 different transitions are possible for each state, then this means that maintainability is being damaged by this strategy. The more transitions you add, the closer we get to an ugly mess.<br><br>Let's address that by combining our 2 solutions above.<br><br>      enum RPS4<br>      {<br>      <br>         Rock,<br>         Paper,<br>         Scissors,<br>         ;<br>         <br>         public static final Map<RPS4, Transitions> lookup;<br>         <br>         record Transitions(RPS4 losesTo, RPS4 winsAgainst) {}<br>         <br>         static<br>         {<br>         <br>            Map<RPS4, Transitions> tempLookup = new HashMap<>();<br>            <br>            for (RPS4 each : RPS4.values())<br>            {<br>            <br>               tempLookup<br>                  .put<br>                  (<br>                     each,<br>                     switch (each)<br>                     {<br>                     <br>                        case Rock      -> new Transitions(Paper, Scissors);<br>                        case Paper     -> new Transitions(Scissors, Rock);<br>                        case Scissors  -> new Transitions(Rock, Paper);<br>                     <br>                     }<br>                  );<br>            <br>            }<br>            <br>            lookup = Map.copyOf(tempLookup);<br>         <br>         }<br>      <br>      }<br><br>So, now the cracks are starting to form. Already, our business logic is getting drowned out by a lot of boilerplate and excess. But all of that boilerplate is "necessary", since it gives us guarantees that we depend upon in order to write correct code. And to add insult to injury, the business logic looks very similar to what my original, hypothetical solution looks like.<br><br>But there's room for improvement -- get rid of the map entirely, and just make the switch expression the entirety of the method body.<br><br><br>      enum RPS4_1<br>      {<br>      <br>         Rock,<br>         Paper,<br>         Scissors,<br>         ;<br>         <br>         record Transitions(RPS4_1 losesTo, RPS4_1 winsAgainst) {}<br>         <br>         public static Transitions transitions(RPS4_1 choice)<br>         {<br>         <br>            return<br>               switch (choice)<br>               {<br>                  case Rock      -> new Transitions(Paper, Scissors);<br>                  case Paper     -> new Transitions(Scissors, Rock);<br>                  case Scissors  -> new Transitions(Rock, Paper);<br>                     <br>               };<br>            <br>         }<br>      <br>      }<br><br>Now, I am a believer that premature optimization causes more problems than it solves. That said, I am a bit uncomfortable with the idea of making a new Transitions object each time this method is called. I will go ahead and assume that the JVM can optimize most, if not all of that cost, but the concern remains. But either way, worst case scenario, we could make private static final fields that house the Transitions.<br><br>Regardless, by using a switch expression, we have essentially created our own little lookup map, but as a method.<br><br>But notice, this Transitions object is returning a lot more than what we want. Which is not a problem at all at this scale. But what if we want our Rock Paper Scissors example to have more details than just transitions?<br><br>Imagine that each enum had other attributes like weight, size, etc. That puts us back into the Catch 21 from before.<br><br>If we stick the extra metadata into Transitions, then we are constructing (maybe, maybe JVM saves us) all of this excess data when we only need 1 or 2 of the fields. And even if the JVM saves us, it doesn't change the fact that we still have to do more hops and unboxing just to get to what I want - the data. There's that indirection creeping back in.<br><br>If we don't stick the extra metadata into Transitions, then we are back to making 2 or more methods to capture what we need, which has the same problems as above.<br><br>And might I remind you, State Transition Diagrams were originally built for (and currently, most frequently used with) algorithms, specifically path-finding algorithms. So performance and memory usage mean a lot to STD's most common domains.<br><br>But, still, there are ways around this too. Java is an incredibly flexible language, as this response has clearly proven. We can get direct relationships and exhaustiveness another way in java -- abstract methods. So let's try that.<br><br>      enum RPS5<br>      {<br>      <br>         Rock{<br>            public Transitions transitions() { return new Transitions(Paper, Scissors); }<br>            public String metadata() { return "whatever"; }<br>         },<br>         Paper{<br>            public Transitions transitions() { return new Transitions(Scissors, Rock); }<br>            public String metadata() { return "whatever"; }<br>         },<br>         Scissors{<br>            public Transitions transitions() { return new Transitions(Rock, Paper); }<br>            public String metadata() { return "whatever"; }<br>         },<br>         ;<br>         <br>         record Transitions(RPS5 losesTo, RPS5 winsAgainst) {}<br>         <br>         public abstract Transitions transitions();<br>         <br>         public abstract String metadata();<br>      <br>      }<br>   <br>For simplicity's sake, I made everything one line. But whether it's one line or multiple, it's pretty clear here that we still have a lot of overhead for what boils down to just simple relationships, right?<br><br>But there's actually another hidden undesirable. In order to appease the abstract method constraint, I had to use an anonymous class for each one of my enum values. Each anonymous class literally creates a new .class file upon compilation.<br><br>For proof, here is my directory of .class files for this example.<br><br>'ScratchPad$RPS4$1.class'<br>'ScratchPad$RPS4$2.class'<br>'ScratchPad$RPS4$3.class'<br>'ScratchPad$RPS4.class'<br><br>Now, this may not seem so bad. But what happens if my STD has 50 states? And what if I have multiple STD's like this? This balloons up very quickly, and can create hundreds or thousands of classes for every single state that I create. And please recall, I spend a lot of my time working with State Transition Diagrams.<br><br>So, still plenty of boilerplate, and now I have a .class file per state.<br><br>If we're going to make a literal .class file per state, why don't we just go all the way and make it a record and a sealed type? At this point, we'd be adding only a little extra overhead, but getting back some simplicity in return and a lot more directness in return. The big reason why going to records makes sense is because we can escape the Catch 21.<br><br>Now, I'll go ahead and skip to the end and say that records and sealed expressions work well enough for my Rock Paper Scissors problem, but we end up recreating a lot of enum functionality. When I had enum's, I had the values() method, EnumSet's, EnumMap's, and more. I have to replace that with a static final field on the sealed interface, IdentityHashSet, and IdentityHashMap, respectively.<br><br>My entire point in showing all of this is to demonstrate all the work we had to do each time our requirements change. Rather than adding another attribute to my enum value's state, I had to rearchitect my solution in yet another way. And this is because the sensical, obvious solution, kept bumping into this circular reference thing, forcing me to uproot my solution and replace it with something different.<br><br>Whereas if we go back to my hypothetical solution, each time the problem changes, the answer becomes obvious, with little to no rearchitecting. We add a field to our enum, we get to keep our exhaustiveness and directness for free, while only charging us a fair tax in terms of conciseness. A clear, obvious solution. And might I add, once we achieve any sort of scale (5+ values), my solution becomes the most concise and easy to read solution.<br><br>But the part that I want to highlight here is this -- each of the solutions that we highlighted are "simple enough" to read. So, their legibility and ease of comprehension is really not my problem here. What bothers me is how maintaining and modifying them is annoying and non-obvious anytime a functionality change request comes in.<br><br>As we travel further and further down the rabbit hole, we find ourselves recreating the things that we used to have for free. Because I didn't have exhaustiveness, I needed a switch expression. Because I didn't have directness, I switched from enums to records and sealed types.<br><br>And for those who think we are at the end of the line, thinking that this is all annoying, but manageable if that is all there is to keep in mind, please note that I have taken you down exactly one path. We went to that path's logical conclusion, but only one path. For example, what if my states are different types because each state needs different fields or a differing number of transitions? There's a whole laundry list of other paths that I didn't even touch, and spoiler alert -- there's surprisingly little overlap between them.<br><br>Hopefully this illustrates my pain points better?<br><br>Thank you for your time and insight!<br>David Alayachew<br></div><br></div>