Dynamic vs Static Tic-Tac-Toe
As part of my application for 8th Light I wrote a C++implementation of tic-tac-toe, or “noughts and crosses” for my fellow Brits. During my apprenticeship I will be writing the same kind of application, with some variations, in a number of languages to get a feel for different language styles.
First up is Ruby. As another object oriented language the basic structure of the code is similar. What is immediately obvious is the much more compact code that a dynamic language with duck typing allows you to write. This is aided by the excellent core libs which provide a wider range of functionality with generally more readable naming.
To take a rather extreme example of this we can compare the functions that detect whether we have a winning line on our tic-tac-toe board.
In Ruby
def winning_line?
lines.any? do |line|
!line.first.empty? && line.all? { |mark| mark == line.first }
end
endWhile in my C++ implementation
class IsWinningLine
{
public:
explicit IsWinningLine(const std::vector<std::string>&
iCells)
: _cells(iCells)
{}
bool operator()(const line_t& iLine)
{
std::string mark = _cells[iLine[0]];
if (mark.empty())
{
return false;
}
auto nonMatchingMark = std::find_if(iLine.begin(),
iLine.end(),
[&](int cell){
return _cells[cell] != mark; });
return nonMatchingMark == iLine.end();
}
private:
const std::vector<std::string>& _cells;
};
bool hasWinningLine(const std::vector<std::string>& iCells,
const int iDimension)
{
auto lines = LINES.at(iDimension);
auto winningLine = std::find_if(lines.begin(),
lines.end(),
IsWinningLine(iCells));
return (winningLine != lines.end());
}I think I know which is more readable!
Actually the core of both pieces of code is the same. We iterate over all the “lines” in the board (rows, columns, diagonals) and look for one where the first “mark” (X or Y) is non-empty and all the other marks match it. The C++ version adds a significant amount of cruft through type declarations and the fact that I defined a functor (IsWinningLine class) to be used in the standard library algorithm std::find_if. Also of note is the ? suffix naming conventions of Ruby which means less of the awkward is/has prefixing in the C++ version.
Of course the downside of this is the lack of safety net which static typing provides. When we write something like line.first.empty? in Ruby we only find out at runtime whether line actually has a first method that returns something with an empty? method. In C++ we would find this during compilation or even in the process of writing with the appropriate tooling. This gives us a classic tradeoff between development speed, code expressiveness and code safety.
As we only find problems when running the code in Ruby it becomes even more important to be running the code frequently. Tests are arguably even more important for dynamic languages because of this. Another good argument for TDD :)