How I would do the “Bowling Game Kata” in Haskell today

By
Posted on
Tags: , , , , , , , ,

Recently, Philip Schwarz from fpilluminated.org wrote to let me know about a new slide deck he had published: The Bowling Game - From Imperative to Functional Programming - Part 1. In the deck, he revisits the “Bowling Game Kata” and summarizes some of the discussion (controversy?) spurred by this exercise over the last two decades. Philip tells this story in a rather clever fashion that blends code, graphics, and comic-book storytelling techniques, so consider checking out the deck.

Anyway, he wrote in particular to kindly let me know that one of the code examples he examines is from my blog, way back when I was learning Haskell in 2006: The Bowling Game Kata in Haskell. This got me thinking about how I might write this code today. So I revised the code, and this is the result:

{-
   My solution to "The Bowling Game Kata"
   Tom Moertel <tom@moertel.com>
   Created 2006-04-05. Revised 2026-07-30 to show "production style."

   See http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata.
-}

module Bowling (scoreGame) where

import Test.HUnit

-- | Computes the score for a player's game having a list of `rolls`.
--   Uses US Bowling Congress rules: https://bowl.com/keeping-score.

scoreGame :: [Int] -> Int
scoreGame rolls = go 0 1 rolls
  where
    go :: Int -> Int -> [Int] -> Int
    go !score !frame rs = case rs of
        _       | frame == 11 -> score             -- 10th frame ends game.
        10:rs'                -> accumFrame 3 rs'  -- Strike scores 3 rolls.
        x:y:rs' | x + y == 10 -> accumFrame 3 rs'  -- Spare scores 3 rolls.
                | otherwise   -> accumFrame 2 rs'  -- Others score 2 rolls.
        _                     -> error "ill-formed sequence of rolls"
      where
        accumFrame n rs' = go (score + sum (take n rs)) (frame + 1) rs'

{-
                      *** Unit tests ***

             *Bowling> runTestTT tests
             Cases: 9  Tried: 9  Errors: 0  Failures: 0
-}

tests = test
    [ "gutters"       ~: score  (rep 20  0)          ~?=   0
    , "ones"          ~: score  (rep 20  1)          ~?=  20
    , "fives"         ~: score  (rep 22  5)          ~?= 150
    , "strikes"       ~: score  (rep 12 10)          ~?= 300
    , "1 + gutters"   ~: score  (1 : rep 19 0)       ~?=   1
    , "first spare"   ~: score  (5:5:5 : rep 17 0)   ~?=  20
    , "first strike"  ~: score  (10:5:5 : rep 17 0)  ~?=  30
    , "last spare"    ~: rscore (5:5:5 : rep 18 0)   ~?=  15
    , "last strike"   ~: rscore (5:5:10 : rep 18 0)  ~?=  20
    ]
  where
    rep    = replicate
    score  = scoreGame
    rscore = score . reverse  -- Scores a reversed list of rolls.

Before I dig into the changes and my rationale for them, let me explain my motivating beliefs about code.

First, I believe the purpose of code is to instruct a computing device what to do in a form that is easy for the code’s intended audience to understand and maintain.

So who was the intended audience for my 2006 bowling code? Then, I was writing the logic mainly for myself and was golfing the solution somewhat in a challenge to see how simply and compactly I could represent it. But my code’s commentary was for people new to Haskell, which at the time was something of a curiosity among programming communities.

This time around, I am writing my code for Haskell programmers who are reasonably “skilled in the art”: they are competent in the language, its standard libraries, and its idioms. That is, I’m writing the code the way I probably would in a production shop that used Haskell.

I point this out because some of the criticism of the functional implementations that are featured in Philip’s deck focused on the use of recursion, and this criticism stemmed from an (unsupported) assertion that recursion is inherently trickier than iteration. Most Haskell programmers who are reasonably “skilled in the art,” however, will have no problem reading or writing iterative processes, such as the one at the heart of my code above, in terms of recursion. In fact, this subject is covered within the very first chapter of Abelson and Sussman’s Structure and Interpretation of Computer Programs, which was used to teach introductory programming at MIT and elsewhere going back to the 1980s.

Second, a note on the problem itself. Both now and back in 2006 when I wrote the original version of this code, I did not do the “kata” as prescribed. It called for developing the solution via Test Driven Development. I am not a fan of TDD because it asks you to develop your logic through the lens of one failing test at a time, the so-called “red-green-refactor” loop. This narrow lens encourages you to view only a tiny slice of your full design space at a time, making it harder to see and exploit the space’s overall structure in your solution. That said, I remain a fan of testing, just not the TDD dogma.

With that in mind, here are the salient changes I made to the code and my rationale for each. Most of them follow from my change in intended audience:

  1. I added a comment saying that the scoring follows the rules published by the United States Bowling Congress (USBC). Whenever your code follows standards or requirements, it is helpful to identify them. That way, if the people reading or maintaining the code have any questions about its logic, they will know how to establish the ground truth.
  2. I removed some comments that explained what the Haskell logic was doing. They made sense for my original code’s Haskell-curious audience, but they don’t for my new code’s audience of more seasoned Haskell programmers.
  3. I renamed a few variables when I thought there was something useful I ought to communicate. The top-level score function became scoreGame; the loop induction variables s and f became score and frame. I did not, however, rename rs and rs’ representing the rolls before and after the current frame is consumed. Those names are fairly idiomatic and are used ten times in a small block of logic. Renaming them rolls and rolls’ would have communicated little more than the current names but would have made that small block of logic unwieldy. I didn’t think the cost was worth the benefit.
  4. I expressed the ten-frame loop in terms of a go function, an idiom which has become common in the two decades since my original version. People unfamiliar with the idiom may believe that calling this function “go” squanders the opportunity to use a name more expressive of the code’s problem domain, but the reality is that go in Haskell is like the for/while/do keywords in other languages: it’s an established way to declare an iterative or recursive process. Calling it something else would have deprived the intended audience of a valuable signal.
  5. I added a type annotation for the go function. This provides a little extra documentation and prevents the frame argument from defaulting to a needlessly large Integer type.
  6. I have added strictness annotations (!) before the score and frame arguments to force their strict evaluation. This ensures that they are always represented by tiny Int values and do not accumulate as a chain of unevaluated additions. (We don’t actually have to worry about this problem for frame because it is compared to 11 on each iteration, forcing its evaluation, but I added the annotation anyway to make its strictness obvious to readers.)
  7. I moved the loop’s termination test into the case expression for slightly greater uniformity and hence slightly improved readability.
  8. I have revised the comments within the code to summarize the USBC rules as they are applied. For example, the comment “Strike scores 3 rolls,” summarizes the official rules: “A strike is marked when you knock down all the pins with your first roll. A strike gives you extra pins as a bonus. You do not add up the score for this frame until you have rolled the ball two more times.” Knowing this rule, for instance, would help a reader to understand why we are passing the argument 3 into the accumFrame call at this point.

I will note that one thing I did not change was the solution’s basic logic. Even though I was golfing a bit on the original 2006 version, I think the form of the solution as it appeared back then was, and still is, a straightforward expression of the rules for scoring a game of bowling.

So that’s how I would write this code today and why. Let me know if you have any questions or concerns with this version.

I would like to thank Philip Schwarz for spurring this discussion. It caused me to think about why I believe the things I do about code and to put those beliefs in writing. (I can’t trust that I understand something unless I can write about it clearly.) If you are interested in reading these writings, on my blog they are tagged with “source code.”