Scala Bowling … more function cowbell

My critiXXXXX advisors expected a more function-oriented solution. Here’s something a bit better, perhaps …

I was at a loss as to how to deal with a function that scores one part of an input list and consumes a different part. Finally I got the notion of a function of two parameters, a total and a list, which adds to the total based on the scoring part and calls itself recursively with the appropriate list remainder.

The result is that I have dropped the Framer class, and added the recursive function to the Game class, which now looks like this:

class Game(rolls: List[Int]) {
   def score = scoreReduce(0,rolls)

   def scoreReduce(total: Int, rolls: List[Int]):Int = {
      rolls match {
           case Nil => total

           case 10::second::third::Nil
               => total+10+second+third

           case first::second::third::Nil if (first+second==10)
                => total+first+second+third

           case 10::second::third::theRest
              => scoreReduce(total+10+second+third, second::third::theRest)

           case first::second::third::theRest if (first + second == 10)
                => scoreReduce(total+first+second+third, third::theRest)

           case first::second::theRest
                 => scoreReduce(total+first+second, theRest)
      }
   }
}

I started with the four black cases. Those pass all my tests but for the perfect game, which was scored 320 rather than 300. What happened was that on the last strike, we score up the 300, but then we return the rest of the rolls (the bonus rolls) for one last go, as if they were a frame. In bowling, the bonus rolls only provide a bonus for a mark in the final frame: they do not constitute a frame of their own.

The two red cases differ from the black ones in that they are explicitly talking about the end of the list: first::second::third::Nil and so on. Instead of recurring one more time, they just return the final value.

So … I feel this is more in the direction of a functional solution. I can imagine going further, and perhaps I will. We have the fact of a two-part argument, the total and the rolls. We might be able to put the total in front, or on the back, and update it there. Front will be easier, as Scala prefers us to work at the head. Let’s see …


Hmm, that turned out to be easier than I thought. I just put the total on the front, and adjusted all the patterns. The result now looks like this:

class Game(rolls: List[Int]) {
   def score = scoreReduce(0::rolls)

   def scoreReduce(rolls: List[Int]):Int = {
      rolls match {
           case total::Nil => total

           case total::10::second::third::Nil
               => total+10+second+third

           case total::first::second::third::Nil if (first+second==10)
                => total+first+second+third

           case total::10::second::third::theRest
              => scoreReduce(total+10+second+third::second::third::theRest)

           case total::first::second::third::theRest if (first + second == 10)
                => scoreReduce(total+first+second+third::third::theRest)

           case total::first::second::theRest
                 => scoreReduce(total+first+second::theRest)
      }
   }
}

I think that is in some sense better. All the work is being done in the patterns, which is perhaps appropriate. The patterns are a bit opaque because of the necessity to embed the two or three rolls of interest into the middle of the list (total, rolls of interest, …). It might be possible to improve that aspect with some typographic tricks.

Anyway, I think it is more functional than it was and I look forward to comments. It has been a long time since I worked in a functional style. (No smart remarks now … :) )

Posted on:

Written by: Ron Jeffries

Categorization: Articles, Practices

12 Responses to “Scala Bowling … more function cowbell”

Jon Bettinger

August 3, 2010

11:57 pm

permalink

I don’t know how you can get away with not counting frames. Consider 8 frames of gutters; strike in 9th; 1,0 in tenth should yield 12. However, 9 frames of gutters; strike, 1, 0 in tenth should yield 11.

Ilmari Vacklin

August 4, 2010

5:46 am

permalink

That looks nice. I understand the code now. :) I’d probably rather use two parameters for scoreReduce: (total, frames), instead of storing both in the same list. That way the recursive scoreRecursive calls would be somewhat clearer.

Ron Jeffries

August 4, 2010

8:15 am

permalink

Jon … see my final version. It doesn’t count frames. I didn’t see how either. Philip Schwarz had the key … don’t count the last frame, recognize it.

Ron Jeffries

August 4, 2010

8:15 am

permalink

Ilmari … yes. See the last version. Storing the total inside was interesting but boneheaded.

Jon Bettinger

August 4, 2010

10:25 am

permalink

I guess I wasn’t clear enough. You can’t reliably recognize it. There is no recognizable difference between 8 frames of gutter followed by 10, 1, 0 vs 9 frames of 0 followed by 10, 1, 0. Both are valid scores for a game.

This test passes:

it(“should count not double count tenth frame bonus rolls” ) {
val game = new Game(List(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,1,0))
expect(11)(game.score)
}

Here’s the failing test.

it(“should count non-mark tenth frame after strike in ninth” ) {
val game = new Game(List(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,1,0))
expect(12)(game.score)
}

Ron Jeffries

August 4, 2010

11:52 am

permalink

Excellent! I knew when working with someone (who?) at some little conference out East, who was doing this in Haskell, that there were edge cases. Nice example.

Now to figure out a solution … ideally w/o counting. Thanks!

Andrew Clay Shafer

August 4, 2010

10:22 pm

permalink

Do it with two functions.

Separate the logic that scores each frame from the accumulation.

First, take the list and return a list with the score for each frame.

Second, sum them up.

Problem solved.

Andrew Clay Shafer

August 4, 2010

10:55 pm

permalink

I realize that the last comment was inadequate.

Here is a more thought out version.

You can’t recognize the last frame as a pattern in isolation, because it is ambiguous, as the bug highlights. It can only be recognized as the last frame in the context of the game of bowling, which happens to have 10 frames. The solution has to have this context.

The simplest thing that I can think of is knowing that bowling has 10 frames, you process the list mapping the patterns into a data structure that has 10 entries, then it’s simple sum.

Andrew Clay Shafer

August 4, 2010

11:27 pm

permalink

You can do it without counting or using the fact there are 10 frames if you process the list backwards…

good times

Andrew Clay Shafer

August 5, 2010

12:38 am

permalink

I lied to myself and others, twice… in one night.

I now believe the solution requires knowing there are 10 frames. Let’s call it ‘business’ logic.

Without that larger context, I don’t see any way to know if the last three scores are 10, x, y, if x and y should be scored once or twice.

Love to see a solution if there is one.

Thanks for the introduction to scala.

Ron Jeffries

August 5, 2010

6:51 am

permalink

There are a couple of important lessons here.

Imagined algorithms often work, while the real ones do not.

Real algorithms often appear to work until the right test case comes along.

I’ll write about this in the next article, unless I do something else.

Ron Jeffries

August 8, 2010

7:30 am

permalink

Closing comments on some of these older articles. Later ones still timely and open …

Recent Articles

Scrum Information Base — Agile Skills

Chet Hendrickson and I have offered to help the Scrum Alliance build a broad and growing base of information relating to Scrum / Agile, and to the many skills and practices that can help teams be successful. Our offer has…

The Gift … a Report and a Request

The Story in Brief

At Agile2011, I brought along a “gift”, a nicely formatted and illustrated Kate Oneal story. I gave a copy to everyone who asked for one, and to a few people who didn’t but who I wanted…

Why Did You Do That, Ron?

Piers Thompson sent me a good question about my recent database articles. I suspect others would like to hear the question and answers.

Events & Classes

  • No events.