Practical Common Ruby

12:04 PM EST Sunday, November 25 2007

As part of my latest effort to learn Lisp, I'm going through Practical Common Lisp. After reading chapter 3, I thought it would be a cool idea to translate that chapter into a language I'm familiar with, Ruby. I'm assuming it will help point out some of the powerful aspects of Lisp, showing how accomplishing the same thing in Ruby is more difficult. So pull up Chapter 3 and follow along at home.

In order to follow along, what I'm doing is using irb, Ruby's version of the Lisp REPL, and saving the code into a script. So create a file called chap3.rb or whatever, and then fire up irb in that same directory:

paulbarry@paulbarry: ~/projects/practical_common_ruby $ irb
irb(main):001:0> load 'chap3.rb'
=> true

Now your script is loaded and you can call whatever methods you have defined. Also, you can run load 'chap3.rb' again and it will reload the script.

So right of the bat, one different we run into is the lack of a property list in Ruby. So in Lisp you have this:

CL-USER> (getf (list :a 1 :b :c 3) :a)
1

It has properties of both a Ruby Array and a Ruby Hash. It's just a list of items, but it's like a Hash in that you can lookup a value by it's key, using the getf function as shown above. So in Ruby, we could use a Hash:

irb(main):001:0> {:a => 1, :b => 2, :c => 3}[:a]
=> 1

Which works for the lookup part, but doesn't preserve insertion order. If we need insertion order, we could use an Array of Arrays and write our own getf function:

def getf(plist, key)
  plist.each do |k, v|
    return v if key == k
  end
  nil
end

irb(main):007:0> getf([[:a, 1], [:b, 2], [:c, 3]], :a)
=> 1
irb(main):008:0> getf([[:a, 1], [:b, 2], [:c, 3]], :d)
=> nil
irb(main):009:0> getf([[:a, 1], [:b, 2], [:c, 3]], :b)
=> 2

In this case, I don't think insertion order is necessary, so we'll just use an Array of Hashes, as it's a little more a part of Ruby.

Now that that's out of the way, we'll set up the global database and the function to make cds:

$db = []

def make_cd(title, artist, rating, ripped)
  {:title => title, :artist => artist, :rating => rating, :ripped => ripped}
end

def add_record(cd)
  $db << cd
end

As you can see, this is just a straight port of the code, which I'll try to stick to throughout. Next up is the dump-db function. Now one little trick that Lisp's format function has is to be able iterate through a list within the format string. This I can say Ruby doesn't have, and it's probably due to the fact that lists are so much a part of Lisp. Here's a more simple example of how it works:

CL-USER> (format t "~{~a~%~}" '(1 2 3))
1
2 
3

Each element of the format string is preceded by a ~ character. So it starts with ~{, which indicates we're working with an element that is a list. The ~} at end just closes the list of things we are doing to each item. The ~a element just effectively prints the argument and the ~% prints a new line character.

As the author points out, this isn't all that different from the ruby % format operator, except for the functionality to iterate over a list. So we'll have to do that ourselves in the Ruby version, but luckily it's not that much work. Here's our first take at it:

def dump_db
  $db.each do |cd|
    cd.each do |k,v|
      puts "%-10s%s" % ["#{k.to_s.upcase}:", v]
    end
  end
end

Here's what that results in:

paulbarry@paulbarry: ~/projects/practical_common_ruby $ irb
irb(main):001:0> load 'chap3.rb'
=> true
irb(main):002:0> add_record(make_cd('Largo','Brad Mehldau',9,true))
=> [{:artist=>"Brad Mehldau", :rating=>9, :ripped=>true, :title=>"Largo"}]
irb(main):003:0> add_record(make_cd('Junta','Phish',9,true))
=> [{:artist=>"Brad Mehldau", :rating=>9, :ripped=>true, :title=>"Largo"}, 
    {:artist=>"Phish", :rating=>9, :ripped=>true, :title=>"Junta"}]
irb(main):004:0> dump_db
ARTIST:   Brad Mehldau
RATING:   9
RIPPED:   true
TITLE:    Largo

ARTIST:   Phish
RATING:   9
RIPPED:   true
TITLE:    Junta

This works except for one issue, the items print out an arbitrary order, since Ruby's Hash doesn't preserve insertion order :(. So we can specify the order in the function if that's something we care about:

def dump_db
  $db.each do |cd|
    %w{title artist rating ripped}.each do |f|
      puts "%-10s%s" % ["#{f.upcase}:", cd[f.to_sym]]
    end
    print "\n"
  end
end

But for the rest of the example I'm going to stick with the random ordered version. The reason I like that it that it prints out everything in the record, even if we add new fields.

So I'm getting tired of re-adding the data, so I'm going to jump down to saving and loading the data.

def save_db(filename)
  open(filename, 'w') do |file|
    file.puts $db.inspect
  end
end

def load_db(filename)
  $db = eval(open(filename){|f| f.read})
end

Here it is in action:

paulbarry@paulbarry: ~/projects/practical_common_ruby $ irb
irb(main):001:0> load 'chap3.rb'
=> true
irb(main):002:0> add_record(make_cd('Largo','Brad Mehldau',9,true))
=> [{:artist=>"Brad Mehldau", :rating=>9, :ripped=>true, :title=>"Largo"}]
irb(main):003:0> add_record(make_cd('Junta','Phish',9,true))
=> [{:artist=>"Brad Mehldau", :rating=>9, :ripped=>true, :title=>"Largo"}, 
    {:artist=>"Phish", :rating=>9, :ripped=>true, :title=>"Junta"}]
irb(main):004:0> save_db "my-cds.db"
=> nil
irb(main):005:0> quit
paulbarry@paulbarry: ~/projects/practical_common_ruby $ irb
irb(main):001:0> load 'chap3.rb'
=> true
irb(main):002:0> load_db "my-cds.db"
=> [{:artist=>"Brad Mehldau", :rating=>9, :ripped=>true, :title=>"Largo"}, 
    {:artist=>"Phish", :rating=>9, :ripped=>true, :title=>"Junta"}]
irb(main):003:0> dump_db
ARTIST:   Brad Mehldau
RATING:   9
RIPPED:   true
TITLE:    Largo

ARTIST:   Phish
RATING:   9
RIPPED:   true
TITLE:    Junta

So now we jump back up to "Improving the User Interaction". This basically translates directly into Ruby, except that we have to create our own y_or_n_p, which is trivial:

def prompt_read(prompt)
  print "#{prompt}: "
  gets.chomp
end

def y_or_n_p(prompt)
  case prompt_read(prompt+" [y/n]").upcase
  when 'Y': true
  when 'N': false
  else y_or_n_p(prompt)
  end
end

def prompt_for_cd
  make_cd(
    prompt_read("Title"),
    prompt_read("Artist"),
    prompt_read("Rating").to_i,
    y_or_n_p("Ripped")
  )
end

def add_cds
  loop do 
    add_record prompt_for_cd
    break unless y_or_n_p("Another?")
  end
end

And here it is in action:

irb(main):021:0> add_cds
Title: Wormwood
Artist: moe.
Rating: 10
Ripped [y/n]: y
Another? [y/n]: y
Title: Animals
Artist: Pink Floyd
Rating: 8
Ripped [y/n]: n
Another? [y/n]: n
=> nil
irb(main):022:0> dump_db
ARTIST:   Brad Mehldau
RATING:   9
RIPPED:   true
TITLE:    Largo

ARTIST:   Phish
RATING:   9
RIPPED:   true
TITLE:    Junta

ARTIST:   moe.
RATING:   10
RIPPED:   true
TITLE:    Wormwood

ARTIST:   Pink Floyd
RATING:   8
RIPPED:   false
TITLE:    Animals

So on to querying the database. We'll use Ruby's find_all iterator rather than Lisp's remove-if-not function. I'll go through each function as the chapter does. First up, selecting an artist:

def select_by_artist(artist)
  $db.find_all{|cd| cd[:artist] == artist}
end

So then we refactor that to pass the selector function (Proc, in Ruby terminology) into the method, and we have a method to create the selector:

def select(selector)
  $db.find_all{|cd| selector.call(cd) }
end

def artist_selector(artist)
  lambda{|cd| cd[:artist] == artist}
end

We call this in Ruby like this:

irb(main):034:0> select artist_selector("Phish")

So next we build the "where" selector, and we'll use Ruby's Hash to stand in for keyword parameters, which are very similar:

def where(p={})
  lambda do |cd|
    (p.has_key?(:title) ? cd[:title] == p[:title] : true) &&
    (p.has_key?(:artist) ? cd[:artist] == p[:artist] : true) &&
    (p.has_key?(:rating) ? cd[:rating] == p[:rating] : true) &&
    (p.has_key?(:ripped) ? cd[:ripped] == p[:ripped] : true)
  end
end

We can call it like this to verify that it works:

irb(main):066:0> select where(:artist => "moe.", :rating => 10)
=> [{:artist=>"moe.", :rating=>10, :ripped=>true, :title=>"Wormwood"}]
irb(main):067:0> select where(:artist => "moe.", :rating => 9)
=> []

By the way, jumping ahead a bit, there's no need to explicitly list each field in the where function. We can simplify that down like this:

def where(p={})
  lambda do |cd|
    r = true
    p.each do |k,v|
      unless cd[k] == v
        r = false
        break
      end
    end
    r
  end
end

So now if you add fields to the cd record, you don't have to touch any of these methods. Onto the update method. This time we'll have Ruby's each iterator stand in for Lisp's mapcar. Also, for the sake of simplicity, we'll modify the actual record in the database, instead of making a copy of the database and updating the global variable to point to the new database.

def update(selector, values={})
  $db.each do |row|
    if selector.call(row)
      values.each do |k,v|
        row[k] = v
      end
    end
  end
end

Again, I feel this is considerably more readable than the Lisp version, plus it doesn't require explicitly listing each field. I suppose readability is in the eye of the beholder. I would imagine Lisp programmers find the end statements in the method above as annoying as Lisp new-comers find the parenthesis in Lisp code, but as you become familiar with the language and the syntax, those annoyances just fade away.

Here it is in action, after adding one more Phish album to the collection, which is never a bad thing:

irb(main):014:0> select where(:artist => "Phish")
=> [{:title=>"Junta", :artist=>"Phish", :rating=>9, :ripped=>true}]
irb(main):015:0> add_cds
Title: Lawnboy
Artist: Phish
Rating: 8
Ripped [y/n]: y
Another? [y/n]: n
=> nil
irb(main):016:0> save_db "my-cds.db"
=> nil
irb(main):017:0> select where(:artist => "Phish")
=> [{:title=>"Junta", :artist=>"Phish", :rating=>9, :ripped=>true}, 
    {:title=>"Lawnboy", :artist=>"Phish", :rating=>8, :ripped=>true}]
irb(main):018:0> update where(:artist => "Phish"), :rating => 7
=> [{:title=>"Largo", :artist=>"Brad Mehldau", :rating=>9, :ripped=>true}, 
    {:title=>"Junta", :artist=>"Phish", :rating=>7, :ripped=>true}, 
    {:title=>"Wormwood", :artist=>"moe.", :rating=>10, :ripped=>true}, 
    {:title=>"Animals", :artist=>"Pink Floyd", :rating=>8, :ripped=>false}, 
    {:title=>"Lawnboy", :artist=>"Phish", :rating=>7, :ripped=>true}]
irb(main):019:0> select where(:rating => 7)
=> [{:title=>"Junta", :artist=>"Phish", :rating=>7, :ripped=>true}, 
    {:title=>"Lawnboy", :artist=>"Phish", :rating=>7, :ripped=>true}]

And for sake of completedness, here's the delete:

def delete(selector)
  $db.delete_if{|cd| selector.call(cd) }
end

And that gives us the whole thing weighing in at 88 lines. It's longer than the Lisp version in terms of number of lines simply because the end statements sit on their own line. We also don't have the duplication that is removed in the final section of this chapter, so that's not necessary.

But the essence of the final section is macros, which seems to be one of the most unique and powerful features of Lisp. In this particular chapter, we've managed to write code that is just as powerful and maintainable, and possibly more readable, without macros. But as I get deeper into Lisp, I'm sure I'll find examples where that's not the case. One observation I have from this so far is that I've never used Ruby's clearly Lisp-inspired lambda feature in my day-to-day Rails work, but maybe I should be.

Posted in  | Tags lisp, Ruby | 0 Comments

LISP - My Personal NBL

6:39 PM EST Friday, November 23 2007

Practical Common Ruby

In the not to distant past I considered myself a "Java" programmer. For my job, I programmed in Java, although I knew and used some other languages to varying degrees, including Perl, PHP, Python and JavaScript. I started to learn Ruby due to all the hype Rails was getting. I really liked Ruby and now program in Ruby almost exclusively, so I suppose I can drop the "Java" from my informal title. I don't think I'll ever call myself a "Ruby" programmer because one of the many things I have learned while learning Ruby is that learning different languages makes you a better programmer in general. You learn new techniques that you can apply to programming in almost any language.

So throughout my career I expect to continue learn other languages. A few that are on my horizon for now are Smalltalk, C/C++, Haskell, Erlang and maybe dive a bit deeper into Python, but for now those are all on the back burner. A few years ago I was programming in Java in my day job and learning Ruby in my spare time. Now I'm programming in Ruby in my day job and learning Lisp in my spare time.

There are several reason for wanting to learn Lisp, but I would say Paul Graham is definitely at the top of that list. I read his book Hackers and Painters and I suggest you do as well. The book is actually a collection of essays, most of which you can read online:

  1. Why Nerds Are Unpopular
  2. Hackers and Painter
  3. What You Can't Say
  4. Good Bad Attitude
  5. The Other Road Ahead
  6. How to Make Wealth
  7. Mind the Gap
  8. A Plan for Spam
  9. Taste for Makers
  10. Programming Languages Explained
  11. The Hundred-Year Language
  12. Beating the Averages
  13. Revenge of the Nerds
  14. The Dream Language
  15. Design and Research

It looks like there are some good free online resources for learning Lisp. I'm gonna start with Practical Common Lisp, and I've also been going through Structure and Interpretation of Computer Programs. It is a computer programming course that is available online. The book is here and videos of the lectures are here.

Posted in  | Tags lisp | 0 Comments

<< Previous Page