|
|
|
May 22nd, 2002.
|
99 bottles of Beer on the Wall
|
While searching for McIntosh and Smalltalk I tripped over the "Old 99 beers on the wall" song, beats me why? However it triggered a memory, Kent is a serious fellow, usually... Who says that Smalltalkers don't have a sense of humor?
Subject: Re: 99 bottles of beer, auf Smalltalk
Date: 1996/01/26
Author: Kent Beck <
There has been a discussion recently on comp.lang.smalltalk about how best to represent the song "99 Bottles of Beer on the Wall" in Smalltalk. Apparently, there has been some use of this song as a way of benchmarking various programming languages.
I found the Smalltalk implementations appalling in their lack of use of the facilities that make Smalltalk so powerful. In particular, the previously published solutions all made heavy use of iteration and conditional logic.
In my quest for intellectual purity, the limits of absurdity, and a decent distraction from this nasty cold, I have undertaken to write the one true definitive Smalltalk version. I hereby place the following code, written in literate program form, into the public domain, to stand as a monument for the ages, or until my hard disk crashes again, whichever comes first.
Specification
We need a program which takes as input, N, a number of bottles of beer and produces as output a string containing the N+1 verses of the bottles of beer song. For N=3, the expected output is:
'3 bottles of beer on the wall
3 bottles of beer
Take one down and pass it around
2 bottles of beer on the wall
2 bottles of beer on the wall
2 bottles of beer
Take one down and pass it around
1 bottle of beer on the wall
1 bottle of beer on the wall
1 bottle of beer
Take it down and pass it around
No bottles of beer on the wall
No bottles of beer on the wall
No bottles of beer
There are no more to pass around
No bottles of beer on the wall
'
Note the subtle changes, verse to verse, that will necessitate the application of powerful object-oriented techniques. The third line changes, as does the plurality of the number of bottles.
Verse
The fundamental question in any object oriented development is "what is my first object?" Until this question is answered aright, nothing else the developer does will be of much use. Fortunately, we have in "song" a natural unit of computation, the Verse. Introducing varying kinds of Verses will allow us to capture with polymorphic messages logic that must be expressed as explicit conditionals in procedural languages. Such future flexibility is of uncertain use with a problem like BOB (Bottles of Beer song), but that's the thing about future requirements, you don't know what they're going to be.
Object
subclass: #Verse
instanceVariableNames: 'stream'
classVariableNames: ''
poolDictionaries: ''
Each Verse will sing itself onto a Stream by singing its verse, then asking the next verse to sing itself.
Verse>>sing
self singVerse.
stream cr.
self nextVerse sing
The recursion ends when you get to a Verse which represents a negative number of bottles of beer, the NegativeVerse. Many developers would have subclassed Verse to define NegativeVerse, but since it need only implement a single message, sing, and since there is no code sharing involved, I chose to subclass directly from object. This leaves me the option to factor Verse inheritance some other way, should that prove important in the future.
Object
subclass: #NegativeVerse
instanceVariableNames: ''
classVariableNames: ''
poolDictionaries: ''
NegativeVerse>>sing
"Do nothing"
We will use other variations on Verse to capture the special cases in the song. PrecedingVerse will sing the Nth to second verse,
PenultimateVerse will sing the verse in which there is one lonely bottle left, and UltimateVerse will sing that sad sad verse in which all the bottle are gone and their contents drained.
The Verse instance creation method is a Factory Method, returning a Verse of the correct subclass depending on the bottle count.
Verse class>>count: anInteger stream: aStream
anInteger < 0 ifTrue: [^NegativeVerse new].
anInteger = 0 ifTrue: [^UltimateVerse stream: aStream].
anInteger = 1 ifTrue: [^PenultimateVerse stream: aStream].
^(PrecedingVerse stream: aStream) setCount: anInteger
Singing a Verse
The singing of a Verse is represented by a method, each line of which causes the singing of one line of the song. In the future, it may be wise to further objectify the solution by creating a family of Line objects which sing individual lines. At this time, however, the
solution does not seem to require this flexibility.
Verse>>singVerse
self
bottlesOfBeerOnTheWall;
bottlesOfBeer;
takeOneDown;
nextVerseBottlesOfBeerOnTheWall
The First Line
The first line is sung by singing the number of bottles, followed by "of bottles of beer on the wall".
Verse>>bottlesOfBeerOnTheWall
self countBottles.
stream
nextPutAll: ' of beer on the wall';
cr
The count of bottles of beer is sung by singing the number of bottles, followed by a space and the word "bottle", appropriately pluralized:
Verse>>countBottles
stream
nextPutAll: self bottleCountString;
space;
nextPutAll: self bottlesString
The default implementation of bottleCountString prints the number of bottles in the receiver:
Verse>>bottleCountString
^self count printString
On the last verse, however, the word "No" is printed, rather than a number. This is represented by overriding bottleCountString in
UltimateVerse:
UltimateVerse>>bottleCountString
^'No'
The pluralization of the word "bottle" is handled by implementing the default plural version in Verse, and overriding in PenultimateVerse:
Verse>>bottlesString
^'bottles'
PenultimateVerse>>bottlesString
^'bottle'
Counting and the Various Verses
The count of bottles is explicitly represented by an instance variable in PrecedingVerse:
Verse
subclass: #PrecedingVerse
instanceVariableNames: 'count'
classVariableNames: ''
poolDictionaries: ''
PrecedingVerse>>setCount: anInteger
count := anInteger
PrecedingVerse>>count
^count
The other verses, subclasses of Verse with no additional instance variables, represent their counts implicitly in the accessing method for "count":
PenultimateVerse>>count
^1
UltimateVerse>>count
^0
Succeeding Lines
The second line is computed in much the same way as the first.
Verse>>bottlesOfBeer
self countBottles.
stream
nextPutAll: ' of beer';
cr
The third line shows variation in both the last and second to last verses. All the other verses sing it with:
PrecedingVerse>>takeOneDown
stream
nextPutAll: 'Take one down and pass it around';
cr
PenultimateVerse>>takeOneDown
stream
nextPutAll: 'Take it down and pass it around';
cr
UltimateVerse>>takeOneDown
stream
nextPutAll: 'There are no more to pass around';
cr
The final line is a bit strange. The last line of one verse is the same as the first line of the next, so the obvious implementation is to send "self nextVerse bottlesOfBeerOnTheWall". However, the result of sending nextVerse to an UltimateVerse is a NegativeVerse, which doesn't know how to bottlesOfBeerOnTheWall. We would either have to duplicate the code from UltimateVerse in NegativeVerse or somehow send a message back to the UltimateVerse. Instead, we explicitly encode the fact that the final two verses have the same last line by hiding it behind a
concatenated message, nextVerseBottlesOfBeerOnTheWall. The default implementation is simple:
Verse>>nextVerseBottlesOfBeerOnTheWall
self nextVerse bottlesOfBeerOnTheWall
UltimateVerse overrides this to send itself the message
bottlesOfBeerOnTheWall:
UltimateVerse>>nextVerseBottlesOfBeerOnTheWall
self bottlesOfBeerOnTheWall
Utilities
Instances of the subclasses of Verse are created by passing along the Stream to be sung on. This is not public protocol, however, and is intended to be used only by the Verse class>>count:on: method.
Verse class>>stream: aStream
^self new setStream: aStream
Verse>>setStream: aStream
stream := aStream
The next verse is computed by decrementing the count by one and sending it to the FactoryMethod in Verse:
Verse>>nextVerse
^Verse
count: self count - 1
stream: stream
Finally, Verse provides a convenient Facade to sing any number of verses:
Verse class>>sing: anInteger
| writer |
writer := WriteStream on: String new.
(self
count: anInteger
stream: writer) sing.
^writer contents
Performance
This program's use of a Stream for contatenation makes it generally efficient. Here is a performance profile run with Profile/V (you were just waiting for the commercial plug, weren't you) and gathered on the recursive call to Verse>>sing.
100% (177) Verse>>sing...
:88% (155) Verse>>singVerse...
::31% (54) Verse>>nextVerseBottlesOfBeerOnTheWall...
:::25% (44) Verse>>bottlesOfBeerOnTheWall...
::::20% (35) Verse>>countBottles...
:::::14% (25) Verse>>bottleCountString...
:::5% (8) Verse>>nextVerse
::26% (46) Verse>>bottlesOfBeerOnTheWall...
:::20% (35) Verse>>countBottles...
::::15% (26) Verse>>bottleCountString...
::22% (39) Verse>>bottlesOfBeer...
:::18% (31) Verse>>countBottles...
::::10% (18) Verse>>bottleCountString...
::8% (15) Verse>>takeOneDown...
:10% (17) Verse>>nextVerse...
NextVerse is redundantly computed twice, so its total of 15% could be cut in half. BottleCountString is computed three times, so its total could be cut from 39% to 13%. This could be further reduced by printing the bottle count directly on the stream, without creating an
intermediate String.
Final Comments
With a bit of work, this program could be expanded to encompass a framework for singing repetitive songs of all kinds. At present, however, it stands as a monument to that sense of the ridiculous that is all that gets us through some times. Long may it wave.
Kent Beck |