Duality has different meanings in different domains, but the essential idea is that of ‘two sides of the same coin’. There is one object, or one entity, and there are different ways of viewing it. The classic literature example is Dr. Jekyll and Mr. Hyde. One person, two different presentations of behavior.
Here we are interested in the mathematical definition of Duality. Let’s say we have a representation of our system, let’s call it A, and another representation of our system, let’s call it B. If we can follow some process f to translate representation A into representation B, and then ‘do the opposite’ to go back from B to A, then A and B are Dual representations of our system.
This has some useful properties. You may want to ask different questions about your system that are easier to answer with one representation than another. One representation may be smaller than another, so you can store it or transfer it more cheaply. With duality, you can go back and forth between system representations and use the one that is most suitable for your needs.
Programmers deal with State and Events all the time, so let’s see how duality can be useful here.
State versus Events
State is a description of your system right now. Events are descriptions of changes to State. State describes a system at a single point in time, and Events describe how a system changes from a single point in time to another point in time.
We’ll use the game of Tic-Tac-Toe to make this more concrete.
In Tic-Tac-Toe you have a board with 9 squares. Each square may be empty, have an X, or have an O. You can change the board by taking your turn. If you are player 1, you play an X on an empty square. If you are player 2, you play an O on an empty square.
The State is – what does the board look like right now? This might just be an array with 9 elements, where each element has a value: ”, ‘X’, or ‘O’.
The Events are…
- Play X on a square (1 through 9)
- Play O on a square (1 through 9)
Let’s go through a few moves to see how it looks.
The State at time 0, before any players have taken their turns, is:
['', '', '',
'', '', '',
'', '', '']
The Event that takes us from time 0 to time 1, Player 1 plays an ‘X’ in the center, is:
{ player: 'X', square: 4 }
Now the State at time 1 is:
['', '', '',
'', 'X', '',
'', '', '']
Event from time 1 to time 2, Player 2 plays an ‘O’ in the top right corner:
{ player: 'O', square: 2 }
Now at the State at time 2 is:
['', '', 'O',
'', 'X', '',
'', '', '']
And so on until the game is over.
The Duality
The whole system here is a single game from start to finish. One way to represent that is to show the board at each point in time.
// time 0
['', '', '',
'', '', '',
'', '', '']
// time 1
['', '', '',
'', 'X', '',
'', '', '']
// time 2
['', '', 'O',
'', 'X', '',
'', '', '']
// etc...
We can see what moves the players made by comparing the board at time t to time t+1. This fully describes the system.
Alternatively, we could represent the system as a sequence of Events.
// time 0 to time 1
{ player: 'X', square: 4 }
// time 1 to time 2
{ player: 'O', square: 2 }
// etc...
These are Dual representations – we can go from one to the other. We can write a function to compare two consecutive States and extract the Events that occurred. And we can write a function to apply the Events to the initial State to recover the board State at any time T (this is Event Sourcing!)
Benefits of Duality
If you are running a hosted game service for Tic-Tac-Toe, you have many different things you need to do with State and Events. Render the board on the client, send moves between client and server, persist the game history so players can view past games, and so on. Some of those may be easier or cheaper to do using the State, and for others it would be better to use the Events.
Quick disclaimer: This is a small illustrative example, meant to show how the Dual representations can be useful in general. Things get more interesting with games that have dozens or hundreds of moves, or other kinds of systems with millions of events or state values. Costs and benefits will be different for every system.
Let’s look at a few of these responsibilities to see how Duality comes into play.
Transferring Game State
When a client makes a move, that gets sent to the server. Then the server needs to verify the move and send the new game state to both clients. It could send the whole board (State), or it could just send the latest move (Event), and the clients could be responsible for applying the move to their last copy of the board, to get the current board.
Generally speaking, the board holds more data than the moves. The board has 9 data points, and a move has 2 data points. It is more expensive to send State than to send an Event. With more complex games, this may be much more pronounced. However, sending Events may require a more complicated protocol. What happens if a client crashes and rejoins, or if there is a network error and a client misses an event? The client needs to be able to query for either the current state or the entire event history if it needs to synchronize with the server.
Persisting Game State
We want to store the game history so our players can review their past games and find mistakes. (This would be more realistic if we were running a Chess service!)
We could store the board at each point in time (State). Or we could store a log of moves (Events). This is similar to the data transfer situation above. An Event has less data, so that is cheaper to store.
Rendering the Board
When we render the board, State may be a better representation. Especially if we are using a state-based rendering system, like React. We need to draw 9 squares, and draw the X’s and O’s in their appropriate squares. That is easy to do with the board. Each array element maps to a board square.
// something like this might do the trick
board.map(piece => <Square piece={piece} />)
Checking for Valid Moves
When a client sends a move to the server, the server needs to ensure the move is valid. This may be easier to do using State. To check if a move is legal, we can just look at the square at the desired location and see if it is empty.
function isValidMove(move) {
return board[move.square] === ''
}
If we use the Events, then we need to iterate through all Events and check if any of them have already made a move to the desired square.
Conclusion
With more complex systems, there’s a lot more we can do with the Duality of State and Events. The cost of transferring data can become non-trivial. Duality gives us options for how to transfer that data, and we can use it to build models that can help us decide which representations to use to minimize that cost. When you perform analytics activities on your system, some queries can be faster to resolve against system State, and some can be faster to resolve against an Event log. Duality lets us choose the best representation for the problem at hand.