Beating Bowling Entirely to Death
Testing Bowling
In a spirit of if some’s good, more’s better, here’s a bit of cleaning up of the bowling code, and some testing. I did the cleaning up first, but let’s look at the testing first.
There is a unit testing framework for Elixir, but I’ve not loaded it yet. I like to start with some direct testing, to get a better sense of the language and to learn how to do things. That said,, here’s a super example of TDDing Fibonacci in Elixir.
Anyway, I just wanted to do a simple expect function that would display whether the answer was correct or not. I’ll tell you the trials and tribulations of that but here’s how it turns out, first:
defmodule BowlingTest do
def expect(expected, expected) do
IO.puts("Expected #{inspect expected} and got it")
end
def expect(expected,actual) do
IO.puts("Expected #{inspect expected} and got #{inspect actual}")
end
def test do
gutters = [0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, 0,0, ]
expect([score: 0], Bowling.score(gutters))
opens = [4,3, 4,3, 4,3, 4,3, 4,3, 4,3, 4,3, 4,3, 4,3, 4,3, ]
expect([score: 70], Bowling.score(opens))
perfect = [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, ]
expect([score: 300], Bowling.score(perfect))
alternating = [10, 6,4, 10, 6,4, 10, 6,4, 10, 6,4, 10, 6,4, 10]
expect([score: 200], Bowling.score(alternating))
alternating2 = [6,4, 10, 6,4, 10, 6,4, 10, 6,4, 10, 6,4, 10, 6,4]
expect([score: 200], Bowling.score(alternating2))
end
end
The cool thing about this is the use of pattern matching in the expect
function. It gets called with two values, and if they’re the same, the first version fires and prints “Expected whatever and got it”, and if they’re not the same, the second version fires and prints “Expected whatever and got alternative”. A near-perfect example of eliminating conditionals using Elixir patterns. Nom nom.
Note, however, that we don’t know exactly what kind of comparison Elixir uses to decide that these two things match. Since Elixir works, it must be pretty strict, and it certainly works for my purposes. When I run the tests, it looks like this:
iex(86)> BowlingTest.test()
Expected [score: 0] and got it
Expected [score: 70] and got it
Expected [score: 300] and got it
Expected [score: 200] and got it
Expected [score: 200] and got it
:ok
So that’s good. It wasn’t as easy as one would think, however. At first I tried this for the output line:
IO.puts("Expected #{expected} and got #{actual}")
And this explodes interestingly. If you do expect(4,2+1)
, you get the expected message about Expected 4 and got 3. But with lists, no such luck. It turns out that IO.puts
only deals with strings. With anything else, you’re on your own. Searching the elixir documentation space for the inspect
trick is fruitless. Fortunately, José Valim (himself), creator of the Elixir language, took pity on me and tweeted me the inspect trick. Which is easy enough if not obvious. Thanks, José!
So I learned a tiny bit and now I can test my bowling implementation. Woot. Impressive, no, but hey, I’m not ten hours into this Elixir thing yet. :)
Cleaning up Bowling
The other thing I did was a tiny bit of cleanup to the code. You’ll recall that to test it originally, I had to (manually) enter something like
Bowling.score(0,0, rolls])
This was tedious, which after a while caused me to do the testing above, but it was also a poor interface: why should you have to put in 0,0 before the list of rolls? Mechanically, it was needed to kick things off but aren’t computers supposed to help us? Well, we can debate that another time, but anyway, there is a nearly-standard way to do better.
We could probably just overload another version of the score
function that takes one parameter, the list of rolls, and it would match at suitable times and do the right thing. But the approved way is to do better, like this:
defmodule Bowling do
def score(rolls), do: _score(0,0,rolls)
defp _score(10, total, rolls), do: [score: total]
defp _score(frame, total, [10, bonus_1, bonus_2 | tail]) do
new_total = total + 10 + bonus_1 + bonus_2
_score(frame+1, new_total, [bonus_1, bonus_2 | tail ] )
end
defp _score( frame, total, [roll_1, roll_2, bonus_1 | tail])
when roll_1 + roll_2 === 10 do
new_total = total + 10 + bonus_1
_score( frame+1, new_total,[ bonus_1 | tail])
end
defp _score(frame, total, [roll_1, roll_2 | tail]) do
_score(frame+1, total + roll_1 + roll_2, tail)
end
end
There are a few parts to this: first, we define a new function at the top that expects the pattern with just rolls. Then we rename all our existing functions with an underbar on the front of their name. (This is just a convention. We could name them do_score or mumble_score if we wanted.) Then in our top level call, we call the new _score function, passing in the starting values of 0,0.
Finally, we use defp
instead of def
, because defp
defines the function privately, so it can only be used from inside.
So, there you are …
A bit of testing, a bit of learning how to print things, and a bit of cleanup. As usual, it took less time to type it in than it did to explain it. This suggests to me that keeping our code as habitable as we can isn’t very costly.
Unless you have to wait for Twitter to get back to you on how to do things. One plans to get over that as one learns. Sometimes it even happens that way.
Thanks for stopping by!