A simple tic tac toe program written in F# inspired by Tony Morris
Here's the original problem and the associated blog post
This is the first approach. It is very clean, and demonstrates a functional
approach to the problem while showcasing composition-over-inheritance
philosophies and why it is important. It represents the domain closely without
relying on dependent typing. In addition, only valid moves are available to be
played - as moves cannot be constructed externally. However, it is possible to
get a move instance from another game and pass it to a completely different
instance of a game that is in progress and allows Play
. This is an issue with
the API design.
This is the OO approach. The approach is leaky due to interfaces being public, and now implementations of those interfaces can be external and thereby not bound to the expected behaviour. Methods are bound to an object and this is very object oriented because the behavior is actually encapsulated inside the class. As an example, how to take back a move is actually encoded in the class.
This is a tighter implementation of the first approach. The discrimintations are no longer present, and we have simplified the game to have optional APIs at compile time. Now, it is impossible to make an invalid move because moves are attached to a particular game and result on creation and is immutable. Making a move always results in the same new state. However, the consumer always needs to check for availability of functionalities, and there is no clear information available from the API on things like, when IFull is present, is it possible for IOver to not be present? This is a problem with the API design which can be solved by following an approach similar to the first, where only valid combinations of capabilities were exposed.
This is an even better approach - here we provide even more information via the
API: for example, takeBack
takes an IUndoable
and returns an IPlayable
,
and both of these are IGame
s. Also, things like, an IFull
(where isDrawn
can be queried, because it's only possible to draw when the board is full) is
always an IOver
(a game that has ended, and allows querying whoWon
) are
communicated more clearly. In the previous approach, when we used discriminated
unions and option types for capabilities like over and full, this was not
possible to enforce. However, in approach 1, that was possible. None of these
interfaces can be publicly implemented by a consumer, because the interfaces
have members which cannot be publicly constructed. We get all the benefits of
the previous approach. Even though it looks like this is object oriented, the
API is fully functional - whereby it just deals with objects as data, and all
functions operate on data. This can be easily understood if the internal type
constructors are thought of as functions that create data of a specific
shape/form.
There are both property-based tests and theories, for all the above approaches. Kindly note that since the API changes across the approaches, the tests may not be exactly the same, but similar cases should nonetheless be covered.
Also, property based tests were added a bit later, so some property based tests may be missing in earlier versions of the source.
To run tests, run dotnet test
in TicTacTony.Tests
directory.
To run the program run dotnet run --project TicTacTony.Console\TicTacTony.Console.fsproj
in project root.
If you do not wish to install dotnet on your platform, you can use docker to run the project.
To build the docker image, run docker build . -t tic-tac-tony
in project root.
To run the console application, run docker run -it tic-tac-tony
in project root.