Applying for a Distant Apprenticeship
Solving a test problem of Tic-Tac-Toe as an apprenticeship application and a shift of my thinking about programming such tasks.
I spent many years of developing software, but something was still feeling odd. People happily used the software, but I was the one that knew that under the cover the process of creating it was more resembling kids playing and experimenting with colored crayons than predictable process steadily increasing the products' value.
Although this may be a bit exaggeration, as I looked into an abyss of desperation of company's "global framework" and in comparison, what I was doing was actually fun most of the time (except for the times when a tiny fraction of the framework was needed in our project and it was inseparable from the rest). I still felt the limits of what I can do the way I was doing it, especially when some architectural change of the system was involved.
I was seriously contemplating starting over in another company when I saw a tweet and a blog post from 8th Light about getting new apprentices that would have the opportunity to learn from 8th Light's craftsmen and one of them being (OMG) Uncle Bob. As I live in Europe I had no illusions I can become one, but there was a test problem to solve attached to the application. I really like solving problems like this. It was a TicTacToe program that has a computer player that never loses.
The first version
I jumped right into the problem. I decided to use Javascript since everything from the code to the GUI can be packed into one HTML file that anyone can run. In addition, I was discovering the Lisp roots of Javascript and the implications of that and that it indeed had good parts.
After one or two hours the first version was ready to be sent. It tackled the problem in a very straightforward way. The entire tree of possible moves was computed (as the problem space is reasonably small) and traversed in a manner of Minimax algorithm (simplified as the entire tree was known) and winning/losing branches were stored in the tree nodes.
Happy with myself I sent the solution back. There was a bit of discomfort about the code having no tests and design fitting just this limited version of the problem, but I had good reasons for that, did not I? How could I develop using TDD Javascript in a webpage and still keep it in one file, right? Or so I thought.
The reply contained two questions: How the code works (as it was indeed clear to me, but having a second look, it was rather tricky to decode) and how can I be sure that it never loses.
I tried to explain as well as I could my solution and how it maps to the code... One thing became clear: Just by making small updates of the code and using a consistent terminology, it could have been much easier to explain. Self-confidence -10%. The next part seemed exciting. How can I be sure it never loses? I know, I did it for several years at the university: I will prove it. So I replied with a detailed and rather long formal (as far as I could tell) proof. Self-confidence +20%.
The second version
The reply had quite a surprising twist: "When I want to make sure it does what I want, I will write a test". Self-confidence -50%. It was so easy and much more error-proof solution than what I spent my time on. Especially in this case when I could just test all the possibilities. But there was a catch. To be able to test the algorithm, it needed some non-trivial changes. These changes would rather be supported by tests as the code was tricky. Thus: I need a testing framework. After some searching and considering different options I came to conclusion to add a minimal test framework for this purpose. I knew that making one is not terribly difficult from Kent Beck's TDD book, where he reconstructs one from scratch. The result surprised me. For illustration, this is what I came up with in a couple of minutes:
var test = { assert: function (x, message) { if (!x) { throw {name: 'TestAssert', message: message}; } }, setup: function (resultElem) { return function (tests) { var name, s = '', passed = 0, failed = 0; for (name in tests) { try { tests[name](); passed += 1; } catch (e) { s += 'Test failed in ' + name + ': ' + e.message + '\n'; failed += 1; } } resultElem.style.backgroundColor = s ? 'red' : 'green'; resultElem.innerHTML = passed + ' passed, ' + failed + ' failed'; if (s) { alert(s); } }; } };
Calling setup() with a DOM element for displaying test results returns a function to apply to an object of test functions. It was all I needed to proceed and it was so simple. The second version was created and tested using it and I was actually much faster than doing the formal proof and it was executable, repeatedly. The proof would become obsolete after certain code changes, tests would not or would tell me so. I was just too focused on rationalizing why I cannot do certain things like unit testing that I forgot to focus on possibilities how I would be able to make them work and what would be the implications.
After this experience my informal distant version of apprenticeship had started. But in fact, it had already been running since I got my first feedback that challenged my experience in quite unexpected ways.