Migrating from java to scala
Libridge and its ongoing migration from java to scala #
Inspiration by lichess #
lichess is an inspiration in many ways. The technical part is one of them.
lichess's creator, thibault, have an interesting article about the choices made during the 10 years he has been working on lichess and what he would do differently if he had the chance:
The description of the repository really caught my eye: "Chess API written in scala. Immutable and free of side effects." - scalachess
After studying and practicing a lot of Functional Programming, I decided libridge should be (in its core) "Immutable and free of side effects." too.
The Chess and Bridge card game domain #
It turns out that the rules of Chess are very well established, the last rule change with real impact in every day game, like en passant or pawn promotions were cemented centuries ago. In fact, go to the scalachess repository and check the number of open issues and recent features and you will see a number very close to zero for this exact same reason. Contrast this with the game server lila and you will see a huge difference.
It turns out that the rules of Bridge (officially called the Laws of Bridge) are also pretty established. Its time period is in the order of decades, not centuries, but is is very rarely changing nonetheless. This creates a domain where values like correctness and performance are more important than values like readability or maintainability.
My personal quest in Functional Programming #
Since I got to know FP some years ago, I found it very interesting and started studying and practicing it when possible. As libridge was always in my mind during this quest, I obviously started seeing the possibilities in this domain. "Hmm, cards in a deck are really something immutable", "Does the PASS card in all of the bidding boxes represent the same thing?", "After a hand is played, will I ever want to change it in any way? If so, should I update it in place or add a correction event?"
Singletons #
In OOP we are usually taught that singletons are evil. This may at first glance be explained because the singleton abstraction itself is not a good abstraction but that is not the case. It is frowned upon because of modularity.
When you define a Singleton for, lets say, an object that represents a database state, you will have difficulties when you try to test its interface, because you will have to substitute this object for another one with the same interface but different implementation. It turns out that when the Singleton is easily constructable, it bears no problem at all for testing if we just construct it every time the application runs. In fact, in Scala, as the three of diamonds is an instance of a case class: if it is needed during the application, it will only be constructed once, and every other invocation of the Card(three, diamonds) will provide a reference to the same object.
One other problem is the lack of encapsulation/control. A Singleton in Java is basically a global variable. But it is here that we find another source of confusion. The problem is not that it is a global variable. The problem is that it is a global variable. It turns out that the three of diamonds is supposed to be global in this domain. The problem arises when someone else can take this three of diamonds and change it into a jack of hearts. That brings us to immutability.
Immutability #
The canonical example for this part is using date objects, but I will stick to the three of diamonds example here. So we have a Card(three,diamonds) that creates only and only one three of diamonds in memory, and after that, distributes references to every code that asks for it again. So, the problem that may happen is: what happens when someone changes the object, or even corrupt it in some way? We can think about overwriting the reference with a reference to another object, or calling a method that mutates its state (something like threeOfDiamonds.changeSuit(hearts)). What happens then?
The scala way is that... It doesn't. By default, what is expected from Scala objects when you call this kind of method on it, is that it returns a new object (cloned from the original) with the expected mutation and changes absolutely nothing in the first object. In fact, an object created from a case class is uniquely defined by its creation parameters (in this case, its rank and suit), so any Card(three,diamonds) not only returns the same .hashCode and true for .equals(Card(three,diamonds)) equality, but also returns true for == equality, as it literally points to the same memory reference.
So, if you want to use the result of a mutation method like the changeSuit example, you would need to attribute the return value of the method to another value, and continue from there:
// Remembering that the changeSuit method does not make sense in this domain
// but it would work like this if it existed:
val otherCard: Card = threeOfDiamonds.changeSuit(hearts)
println(threeOfDiamonds) // Card(three,diamonds)
println(otherCard) // Card(three,hearts)
Auditability? #
Previous versions of the "undo" feature worked by updating the state of the hands to change the cards that were played, move them back from the table to the hands, etc. This would inevitably lose information on what happened, like, what was the original card that was played? This is usually very important when this kind of event happens in a physical table, I figured it would be at least as important in an online match. So, during the quest I started to see this feature (and many others) as a derivation of a List of events, not as a state of an object. Imagine something like:
...
North played the three of diamonds
North asked for an undo
West allowed the undo
East allowed the undo
North played the jack of hearts
So, we record this CardPlay as a list of Events, and when we need to show the state - imagine the cards in a player's hand - we calculate its derivation. Calculating the current Hand of a player from a list of events is pretty simple and fast, but even if that becomes a bottleneck in the future, it could easily be improved: using hashes for intermediate calculated derivations is one that comes to mind.
This is not a new idea, in fact, I had already been introduced to it by Bruno Castanho and Kaique Vieira in a previous job. It is called Event Sourcing, and the derived state in a specific time is called a Projection. At the time, I saw the potential of the idea, but it took me years for me to see this specific implementation and the value it brings.
One extra feature for using this, is that we can add timestamps to each of the events, and have a complete log for every hand played in the game. This would be excellent to help with cheat prevention and also to create statistics on the time that players spent thinking in each decision of the game. This could start in the auction and card play but can go all the way up to browser interactions.
...
100s North played the three of diamonds
102s North moved the focus to another tab of the browser
105s North moved the focus back to the game tab of the browser
106s North asked for an undo
110s West allowed the undo
111s East allowed the undo
112s North played the jack of hearts
This is a simple example that would be helpful for a tournament director to rule on a foul play claim. In a real life example, we could have timestamps in the orders of milliseconds, and the mere change in tempo of a decision - lets say a hard decision that took less time than usual - could be helpful to catch cheaters. Multiply that for many hands in a tournament, and compare it to the time it took for all other players to make that same decision and you can see why it would be very useful to have a complete, unadulterated history of every single event in the table.
Add that to the fact that Bridge is a game played one Hand at a time, and you will have records that are very small - lets say less than a thousand events for one hand - and yet contain every information ever created. Also, as this information is not needed in real time, you can just store it away, and process it when it is feasible to do so. This way you free your runtime memory without losing any information whatsoever.
Value Oriented Programming vs Place Oriented Programming #
This talk from Rich Hickey was probably the one that made the largest amount of functional programming ideas in my head solidify into a good understanding of the paradigm. In summary, the idea of Value Oriented Programming means that we should shift our abstraction from "a place that holds the information" to "the information itself". The complete reasoning and motives are available in the talk, but I can assure that, even if you don't end up using it, it will change the way you think about storage/memory in programming.
In conclusion: So what? Java can do all that... #
Yes, it does. But it is not made for it. Let's compare these two versions of the Hand class. In the first one I tried to force immutability using Java. In the second one I used scala's built-in capabilities.
Take a look at this snippet of code from libridge-backend
public Hand addCard(Card card) {
List<Card> newCardsList = new ArrayList<Card>(this.cards);
newCardsList.add(card);
return new Hand(newCardsList, this.playedCards);
}
and then compare it to the equivalent code in scalabridge
def addCard(card: Card): Hand = this.copy(allCards = allCards + card)
We can clearly see that the scala code is better in almost any metric you can imagine. It is shorter, it is more readable, is is simpler (as opposed to complex), it is easier to verify its correctness, you don't have to worry that you forgot something and there is a hole in the shell so a client could mutate your object. In short, it reduces your cognitive complexity: it makes you have to think less. You may add a criticism that scala (and functional programming) is less widespread and would have more difficulty of being understood because of that, but this is the only thing I can imagine adds to a "con" for the FP code.
The rest of the migration #
Libridge has thousands of lines of java code and the incentive to translate it for scala is not really there for all of them. The most valuable candidates have already been migrated to scalabridge, and it will continue for a while, specially for the core classes. But definitely not for all of the classes. For instance, I intend to keep the classes that are supposed to be mutating all the time in Java, not only because Java does it better, but because the code is already there.
If you got all the way here, chances are you are interested in bridge, in programming or in both. Send me an email at contributing@libridge.club or take a look at the code if you would like to contribute to the project.
- Previous: First post