Clear questions and runnable code get the best and fastest answer |
|
PerlMonks |
Drunk on golf: 99 Bottles of Beerby eyepopslikeamosquito (Archbishop) |
on May 08, 2011 at 12:41 UTC ( [id://903641]=perlmeditation: print w/replies, xml ) | Need Help?? |
🥴 🍻 🏌 ⛳ Apart from its popularity as a drinking song, the classic 99 Bottles of Beer is ideal for singing to while away the hours on long trips because it takes a long time to sing and its repetitive format is easily memorized. It also pops up often in popular culture, especially film and TV. I remember the late Patrick Swayze, for example, singing it over and over again to Whoopi Goldberg in the movie Ghost in order to get her to go downtown (which she would never normally do).
While the affection for this song among computer programmers may have begun in 1977, with the publication of Knuth's seminal paper, it didn't become wildly popular until 1994, when some nitwit posted the entire lyrics of the song to a humour mailing list, provoking a BASIC version to be written: to "save mailing list bandwidth". It snowballed from there until today we can now choose from more than 1400 programming language variations. Over the years, many different Perl solutions have been proposed. On December 25 1998, for instance, Damian Conway composed a version using his Lingua::EN::Inflect module:
Finally, we come to the reason for this node. Despite being the most popular code golf game of all time, precious few quality bottle golf solutions have ever been published. While there are vast hordes of really awful bottle golf solutions out there, the only half way decent one I'm aware of was whipped up in a few hours by Ton Hospel and Mtv Europe way back in 2003:
Many top quality golf solutions have been concocted though. For example, the 99 Bottles of Beer challenge at codegolf.com attracted over 1000 entries, more than twice as many as the next most popular game. And the leading scoreboard entries there are very short. It's just that none of them are available for public viewing. Being a repeat codegolf vandal, I'm not shy about publishing my codegolf entries here, especially given the codegolf site seems to have been abandoned, and their beer bottle challenge has been running for over five years now. There is one caveat though: after discussions with the founders of this new PHP golf site, I won't be discussing my PHP solutions in this node, so as not to spoil their most popular game. Apart from that, I won't hold back, analysing general approaches to writing the shortest possible code to emit the lyrics of the classic drinking song and revealing all interesting Perl, Ruby and Python solutions I found while playing this game. Rules of the game In this 99 Bottles of Beer challenge, your program takes no input and must emit the entire lyrics of the song to stdout as follows: Your program's stdout must match the above output exactly, with one exception: the codegolf.com entry checking program strips trailing whitespace, so emitting any amount of trailing whitespace is permitted. Note that anything written to stderr is ignored, as is the program's exit code. So terminating your program by crashing it (dividing by zero, for example) is legitimate ... and surprisingly common and strangely satisfying. Getting Started
🏌 ⛳ 🍺 The five times this game has been played here at Perl Monks:
and: If you squint, you'll see that these two are based on the venerable 2003 solution of Hospel and Europe. Given that the leader at this time, Polish golfing veteran "0xF", had already posted a 165-stroker, I was not hopeful that this approach could seriously threaten this experienced Polish golfing maestro. Still, I had to try at least. First, I noticed that Ton and Mtv's snippet can be further shortened by changing "s" to 's' and embedding the sub inside the first @{} block like so: This saves a single stroke because the first +b in the original is replaced by a bald b. I then stared at the kldp solution, looking for anything unsightly. Well, that -99..-1 is an eyesore ... so I just hacked it out, reducing this solution from 174 to 168 strokes: Notice that this solution exploits undeclared Perl variables (i.e. $n above) being initialized to undef. I employed this tactical trick time and time again in this game in both Perl and PHP ... though not in Python, where it can't be done. You can pull it off in Ruby too, though you must scour RubyDoc for any "Perl-compatible" Ruby special variables (e.g. $., $_, $&, $', $`, ...) that might be gainfully employed. Around this time, I also composed Python (195) and Ruby (191) equivalents of my early Perl subroutine-based solution. Note that in the Python solution: string slices are employed, which is almost mandatory when golfing in Python. Though my first Ruby attempt: is ungainly in the extreme, notice the curious expression: This syntactic quirk, where an assignment expression, sans parentheses, can be placed at the end of an expression, is used ad nauseam in both Ruby and PHP golf ... though not in Perl or Python, where it produces a syntax error. I felt quite hungover around this time because these solutions were over ten strokes behind in Python and nearly twenty behind in Ruby! It was becoming increasingly obvious that using a function was consuming far too many strokes and therefore had to be eliminated. Less obvious in Perl perhaps, given I was only three behind, but my golfing instincts were telling me that a Perl solution without a sub should be considerably shorter than my 168-stroke subroutine-based solution. Though I reached this conclusion some years ago, I didn't have the time to pursue it back then, and so put this game on hold for a little while. Actually, I forgot about it for several years until "reminded" of it by approaches from the founders of a new PHP golf site, where they were hosting the identical game. When I returned to this game earlier this year, I noticed that "Rhebus", a young whipper snapper from London, had posted a sensational 162 strokes, thus snatching the Perl lead from the grasp of grizzled Polish expert 0xF. Bottle Golf Tip No 1: Don't use a function To eliminate the function, you need somehow to set up the loop so that the count of beer bottles is the same for each loop iteration. That is, instead of the "natural": where each paragraph must emit both n bottles and n-1 bottles, you might try breaking up the song as shown below: That's better! Note that, crucial for a function-free solution, the two lines between the lines of dashes above now contain the same beer bottle count. There is a catch though: you must deal somehow with the annoyance of the first iteration, which does not contain a Take one down... line. The last line is also a pest, but can be dealt with simply by printing it after the main loop has terminated. As a variation, you can place the pestiferous last line first: which nicely clarifies the essentially circular nature of the ditty. The "uniformity" of this output is attractive for a golfer because uniformity often means shorter code. To exploit this uniformity, however, you cannot just print the song on the fly; you must instead build a string or list in a loop, then print two separate bits of it at the end. Let's see how this might look in Perl: Hmmm, 173 strokes. Not bad for a first attempt!
Regexes always win (Mtv's law of golf) As you can see, the regex "side-effect variables", $' and $`, look very promising in this game -- as they so often are in golf. It seems that Mtv's law applies to Ruby too, because this algorithm is actually better suited to Ruby than Perl due to some quirky Ruby syntactic sugar. You see, in Ruby: can be equivalently expressed as: So, in Ruby, this new function-free approach effortlessly shaved eleven strokes from my earlier function-based solution: 180 strokes! Though hardly the shortest, this is one of my favourite solutions because it directly expresses the (circular) idea of putting the last line first. Update: Much later I reduced this by three strokes to 177:
What about Python? Alas, Python places many obstacles in the way of this approach:
it's 207 strokes in length. Ouch. Of course, it's perfectly possible that I've overlooked some more golfish Python list comprehension syntax here. If so, please respond away. After giving up on a functional approach, the shortest way I could find to implement this idea in Python was the following 193-stroker: Though admittedly two strokes better than my original function-based entry, this solution is a gaping eleven strokes behind the lead, 182 strokes, held by Norwegian teetotaler "hallvabo" -- who I contend has nowadays taken over the mantle of No. 1 Python golfer from Mark Byers. In desperation, I tried a completely different line-at-a-time function-free approach in Python: To my enormous frustration, this solution, in addition to being unspeakably ugly, turned out to have exactly the same score, 193 strokes. No improvement. At all. Aaargh! The 187-stroke Ruby translation below is presented as an example of an accepted solution that "cleanly terminates" by dividing by zero: Note that the three stroke 198 above is routinely replaced by the two stroke ?ascii-char-with-ord-198 when golfing in Ruby. Bottle Golf Tip No 2: Consider counting from 1 up to 99 not 99 down to 1 Though counting down from 99 to 1 seems "natural", golfers should always experiment with switching things around, to see if doing so shortens the code.
General golfing tip: Look for the part of your solution that makes you pull a face and try to shorten it. With the possible exception of Python, all the above attempts at solving the crucial "1 bottle" versus "2-99 bottles" plural inflection problem have caused me to pull a face. Here is the parade of horrors seen so far:
As you might expect, I was eager to eliminate all this inflective claptrap, replacing it with something simple and short like: where $x is the empty string for n==1 and "s" for n>1. Can this be achieved? Well, in languages where uninitialized variables display an empty string, it's easily pulled off simply by counting upwards from 1 to 99 ... so long as you can find a short way to set $x to "s" right after the first iteration. Of course, you'd need to build the output string in a loop and emit it at the end, yet the regex solutions described above already do that, courtesy of the $' and $` special variables. Instead of $x, how about applying Mtv's law yet again and (indirectly) exploit a regex variable? As is common in golf, once I'd thought of the basic idea, a short solution materialized pretty quickly: 165 strokes! Caught 0xF at last! Here we're building the string from the end to the beginning, from 1 to 99, in $_ by using the regex substitution s/^/.../. We're also exploiting a starting value of zero for the uninitialized $n. Finally, note that we're further exploiting an aliasing quirk of the perl for loop by building a single $_ value via a ($_)x99 list. I remembered this surprising trick from the 2002 Cantor game in TPR03 (see Mtv's pdf book of Golf), where a winning Cantor solution was:
After a further period of shuffling things around, I finally tied for the lead at 162 strokes by replacing for with until like so: Note that the /, 99.*\n\n/ expression serves a dual purpose in that, in addition to terminating the until loop, this regex sets the $' and $& special variables to the desired values. Note further that if leading whitespace were allowed (as it is at phpgolf.org), this regex can be shortened by two strokes to /, 99.*/. Further 162-stroke variations are: and:
I have an uneasy feeling that this solution is not the end of the road and that a future Perl golfer may further shorten it. Can we translate this approach into Ruby? Yes. It's not as effective as the Perl version because Ruby lacks Perl's built-in s/^/.../ substitution operator. But it still shaves two strokes off our previous best Ruby solution: 178 strokes. And here's an alternative 178-stroker using until: The need for a space before until is unfortunate. As is the need for the n=0 initialization, which is not required in Perl. As already discussed, I saw no hope of translating this approach into Python, where short "special variables" (e.g. $_, $&) do not exist and where regexes are not built into the language. Bottle Golf Tip No 3: Unearth language-specific tactical tricks My previous golfing articles focused mostly on the dark art of magic formulae. As you may have noticed above, magic formulae hardly feature in this game. You must rely instead on old fashioned classical golf technique, especially language-specific tactical tricks.
One of the best ways to find tactical tricks in the dusty corners of a programming language is to simply try ridiculous things and see what happens. Modern languages are so large and complex that it's not practical to precisely specify every little detail in the core language and libraries ... which offers opportunities for golfers to exploit implementation quirks. Desperate to save a stroke or two, I experimented with how the % printf-like operator behaves when given more arguments than expected: Running this little test program produces: in Ruby, but: in Python. In case you're interested, running this little test program in Perl: produces the same result as Ruby. That is, Ruby and Perl silently ignore extra arguments to printf. They also automatically "flatten" the array. Python does not. Python also checks the number of printf arguments more strictly; that is, this Python program: fails with: whereas this one: works, printing:
I was able to exploit Ruby's printf behaviour, described above, to shave two precious strokes and so move into outright second place, three strokes behind "Ruby golfing god", flagitious: 176 strokes! Cheers! Staggering into second place in Ruby was a pleasant surprise after languishing twenty strokes behind the leaders early in this game. I hope you've enjoyed my long journey through this game. It's time for me to sober up now and give the game of golf a rest for a while. Leaderboards, May 2011 All languages (1237 entries):
Perl (273 entries):
Ruby (338 entries):
Python (423 entries):
PHP (264 entries):
References
Added later:
Update: As described in detail in a three part series Compression in Golf: Part I, this 160-stroke Perl solution was later whittled to around 150 strokes, by using pack u compression.
Back to
Meditations
|
|