Today I gave myself a little kata to work at (namely Happy numbers from the CCD-School).
That in itself is not so special. The real fun started after I technically finished the kata.
A few days ago I read blog posts about "Tell, dont ask", and a related idea called something like "east oriented [coding]".
The basic idea is that all data should "travel east" in the code. This is what I liked to try and it has two main implications for the code:
- it effectively forbids assignments, because that lets the data travel west (var x=SomeFunction(); calculates the result, and then stores it in x - which is "west" of the call).
- functions return void; which only lets you get stuff done by using continuations (Note: this is an exaggeration of the original concept, which talks about object orientation and methods that should return void or the result object; However in the context of this blog let me stay without return values...).
That sounds like live will get functional - and I regret having started the kata with C#...
The initial solution consisted of a function that returned whether the number is happy, or not:
bool IsHappy(int p) { ... Clearly west bound; so I change it to
void IsHappy(int p, Action<int> act) { ...
which immediately breaks all tests. No surprise.
(As a side note: I actually first had an Action<bool> in it but it turned out that the bool is not very helpful. The called action function would know that something is happy, but not which number. It seemed to make way more sense to only call the action when the number is happy; with that number as an argument)
Ok, I change the first test from
Assert.IsTrue(IsHappy(1));
to
IsHappy(1, x => Assert...
um, no that would not work because it would pass even if the lambda was never called. So how do you check whether a function is never called? The action needs to leave something that I can check; like this:
var called = false;
IsHappy(1, x => called = true;);
Assert.IsTrue(called)
However, this is west bound again. The assignement, you know... The solution I found uses an exception. Let the test expect an exception, and the call throw one. If its never called - it fails because of the missing exception. Interestingly this is less code, although it feels odd to throw an exception when things work:
[ExpectedException(typeof(Exception))]
...
IsHappy(1, x => { throw new Exception(); }); The negative tests are easier, they just Assert.Fail() when the lambda is called.
The actual algorithm is pretty easy to convert: basically replace all return(s) by the call to our continuation action or dont do anything. However, what bit me first after that was that - other than the return statement - this call obviously returns. This creates a few surprises, but is easily fixed.
Next the test that checks the returned value fails - it is always one. Crap. After the first recursion the function does not know the initial value any more and as 1 is the value that stops the recusion - this is what always is passed on. This calls for a helper function to keep that value in the loop. Noteworthy that this is easier in functional languages such as F# or lisp where you can use local functions; possible in c# as well, but it looks a little ugly (and has an assignment because the function needs a name when you like to recurse; well, if you do not resort to y- or z-combinators...). No use bothering with further detail... here is the final code of my solution (without the tests):
public void IsHappy(int value, Action act)
{
IsHappyHelper(value, value, act);
}
private void IsHappyHelper(int value, int current, Action act)
{
if (current == 4) return;
if (current == 1) act(value);
else CalcNext(0, current, x => IsHappyHelper(value, x, act));
}
private void CalcNext(int sumsofar, int p, Action act)
{
if (p <= 0) act(sumsofar);
else CalcNext(sumsofar + (p%10)*(p%10), p/10, act);
}
All in all:
Disclaimer: This was an experiment! That means I did stuff here, that I would not normally do in production code. I also know that Tell-dont-ask and east-bound are originally object oriented; in this kata I kind of used them in a functional context. DO try this at home... ;-)