In software development there are no silver bullets, but I am always looking out for the next bronze one.

Friday, May 25, 2007

Bowling with Erlang and Scala

I read an blog entry (http://www.randomhacks.net/articles/2007/04/28/bowling-in-haskell) that was picked up by dzone.com demonstrating how to keep score of a bowling game using Haskell. I modified the Haskell version to use the Maybe data type to allow scoring of incomplete games and then ported the code to Erlang and Scala to get a feel for the language feature. If anyone is interested, here is the Erlang version I came up with:
-module(bowling).
-export([tallyGame/1]).

scoreToInt(X) ->
case X of
none -> 0;
_ -> X
end.

take(N,L) ->
if (N > 0) ->
case L of
[X|Rest] -> [X] ++ take(N-1, Rest);
[] -> []
end;
true -> []
end.

reduce(F,YS) ->
if
(YS == []) -> [];
true -> {X, YS2} = F(YS),
[X] ++ reduce(F,YS2)
end.

invalid_pin_count() -> throw("invalid_pin_count").

scoreFrame(Balls) ->
case Balls of
[X|_] when X <> 10 -> invalid_pin_count();
[_,X|_] when X <> 10 -> invalid_pin_count();
[X1,X2|_] when X1 <> 10) -> invalid_pin_count();
[X1,Y1,Y2|Rest] when (X1 == 10) -> {(X1+Y1+Y2), [Y1,Y2] ++ Rest};
[X1,X2,Y1|Rest] when ((X1 + X2) == 10) -> {(X1+X2+Y1), [Y1] ++ Rest};
[X1,X2|Rest] -> {(X1 + X2), Rest};
_ -> {none, []}
end.

scoreGame(Balls) -> take(10,reduce(fun scoreFrame/1, Balls)).

tallyScores(Accum, L) ->
case L of
[] -> [];
[Head|Tail] -> Total = (Accum + scoreToInt(Head)),
[Total] ++ tallyScores(Total, Tail)
end.

tallyGame(L) -> tallyScores(0, scoreGame(L)).



Run this in "erl" via "bowling:tallyGame([<rolls>])". Example:


bowling:tallyGame([10,10,10,10,10,10,10,10,10,10,10,10]).
[30,60,90,120,150,180,210,240,270,300]

With some feed back from members of the Scala lounge mailing list, this is the Scala version:



object FunctionalBowling {
type Score=Option[int]
type Balls=List[int]

def reduce[A,B](f:(A => (B, A)), ys:A): List[B] =
if (ys == Nil) Nil
else {val prime = f(ys); prime._1 :: reduce(f, prime._2)}

def scoreGame(balls:Balls): List[Score] = reduce(&scoreFrame,balls).take(10)

implicit def scoreToInt(score:Score): int = score getOrElse 0

implicit def intToScore(in: int): Score = Some(in)

def invalidPinCount() = throw(new Exception("Invalid Pin Count"))

def scoreFrame(balls: Balls): (Score, Balls) = balls match {
case (x :: _) if (x <> 10) => invalidPinCount() // Invalid first roll
case (_ :: x :: _) if (x <> 10) => invalidPinCount() // Invalid second roll
case (x1 :: x2 :: _) if (x1 <> 10) => invalidPinCount()
// Total pin count for spare frame can't be > 10
case (x1 :: x2 :: x3 :: rest) if x1 == 10 => (x1 + x2 + x3, x2 :: x3 :: rest)
case (x1 :: x2 :: x3 :: rest) if x1 + x2 == 10 => (x1 + x2 + x3, x3 :: rest)
case (x1 :: x2 :: x3 :: rest) => (x1 + x2, x3 :: rest)
case (x1 :: x2 :: Nil) if x1 + x2 < 10 =""> (x1 + x2, Nil)
case _ => (None, Nil)
}

def tallyScores(accum:int, x:List[Score]):List[int] = x match {
case Nil => Nil
case x :: rest => val total = accum + x
total :: tallyScores(total, rest)
}

def main(args:Array[String]):Unit = {
val roll_list = args.toList.map(Integer.parseInt)
System.out.println("Scores: " + tallyScores(0,scoreGame(roll_list)));
}
}

Invoke the Scala version via "scala FunctionalBowling <rolls separated by space>". Example:


scala FunctionalBowling 10 9 1 10 9 1 10 9  1 10 9 1 10 9 1 10 9 1
Scores: List(20, 40, 60, 80, 100, 120, 140, 160, 180, 200)