The Fun Never Stops With Ruby

Posted: Mon, 21 November 2005 | permalink | No comments

Well, there's nothing like posting a really rubbish program to your blog to find out the right way to do it.

Firstly, Philipp Kern has come up with another, even shorter version than his version I posted in the update to my original Ruby post, this time blowing me away with the strangeness of explicit casts in Ruby. Apparently, casting an input stream to an array produces an array with one-line-per-element, as in this example:

 STDIN.to_a.sort_by{rand}.each{|l|puts l}

As Philipp warns, however: "Ruby is full of fun and power; one must watch out that the scripts remain readable". For a fairly toy example, this solution is still fairly readable, however I can see hair growing on this thing if your program grows large.

Next, Pierre-Charles David showed his qualifications for becoming a co-author of the next edition of the Pickaxe (AKA "Programming Ruby: The Pragmatic Programmers' Guide"), with his example and explanation. He's kindly given permission for me to reproduce his e-mail in it's entireity, since it's too good to chop up:

Here is the shortest and most readable version of your "randomize stdin" script I can think of:

  #!/usr/bin/env ruby
  print ARGF.readlines.sort_by { rand }

ARGF is a special variable representing either stdin or, in the case where file names are passed on the command line, the content of these files (think of Perl's <>). ARGF behaves like an IO object (although I think it is not one). In particular, it responds to the #readlines method which splits the receiver's content into lines and returns an array. In other words,

  lines = ARGF.readlines

is the idiomatic way of doing

  lines = []
  while line = gets() do
    lines << line

in your example.

The next part (sort_by) uses a new form of the sort method, which I think is not documented in the Pickaxe. It implements the Schwartzian transform technique. In essence, a random number is associated to each element of the array, which are then sorted based on this index.

As a general style remark, note that although Ruby supports "while" and "for x in y" loops, they are rarely used, in favor of iterators and/or higher order methods. For example, to gather all the input lines in an array without using readlines, one might write

  lines =
  ARGF.each_line { |l| lines << l }


  lines = ARGF.inject([]) { |lines, l| lines << l }

Hope it helps, and welcome to Ruby!

Pierre, your explanation definitely helped, and I hope it helps others too.

The third person to help me out is Decklin Foster, through a post in his blog. What particularly caught me about his solution was the neat demonstration of another feature of Ruby which I think I will really learn to love: the ability to dynamically add methods to an existing class. "Aiee!" I hear all the purists scream -- and I can see the potential for severe misuse, too. But, since every good idea seems to get deeply perverted, we may as well have some real useful features to pervert while we're at it.

Decklin has the clever idea of adding methods to the Array class that shuffle the elements of the array in a random fashion. His code is beautiful in it's simplicity:

 class Array
    def swap!(a, b)
        self[a], self[b] = self[b], self[a]
    def shuffle!
        each_index do |i|
            j = i + rand(length - i)
            swap!(i, j)

He then goes on to rewrite my bumbling example in probably the most comprehensible manner I've yet seen:

 STDIN.readlines.shuffle!.each { |line| puts line }

Decklin writes, "Now that is cool.". I couldn't have put it better myself.

My thanks to Philipp, Pierre-Charles, and Decklin, for helping me along the Ruby path.

So, two new Ruby Rules for me to live by:

Hmm, is it possible that Ruby is nothing more than Functional Programming by Stealth? Aiee!

Post a comment

All comments are held for moderation; markdown formatting accepted.

This is a honeypot form. Do not use this form unless you want to get your IP address blacklisted. Use the second form below for comments.
Name: (required)
E-mail: (required, not published)
Website: (optional)
Name: (required)
E-mail: (required, not published)
Website: (optional)