Ruby Tutorials

Tutorials on Ruby specific concepts.

5

JAN
2006

Code as a Data Type

Introduction

This is the first of a series of articles where I will try to demystify some Ruby idioms for the people who come to Ruby through Rails and find themselves wanting to learn a little more about the language under the hood.

Strings, Arrays, ... and Code?

You don't have to code for long in any language before you get intimately familiar with some standard data types. We all have a fair grasp of Ruby's String and Array, because every language has something similar. Ruby has an unusual data type though, which can trip up newcomers. That type is Ruby code itself.

Allow me to explain what I mean, through an example. First, let's create a little in-memory database to work with:

class ClientDB
  Record = Struct.new(:client_name, :location, :projects)

  def initialize
    @records = [ Record.new( "Gray Soft", "Oklahoma",
                             ["Ruby Quiz", "Rails Extensions"] ),
                 Record.new( "Serenity Crew", "Deep Space",
                             ["Ship Enhancements"] ),
                 Record.new( "Neo", "Hollywood", 
                             ["Rails interface for the Matrix"] ) ]
  end
end

Of course we need to be able to query this data. Let's add a very basic query routine:

class ClientDB
  def select(query)
    # parse query String
    field, value = query.split(/\s*=\s*/)
    value.sub!(/\A['"](.+)['"]\z/, '\1')

    # match records
    @records.select { |record| record.send(field) == value }
  end
end

Finally, we should be able to make some queries:

require "pp"
db = ClientDB.new 
pp db.select("client_name = 'Gray Soft'")
pp db.select("location    = 'Hollywood'")

We have the beginnings of a system here, but my queries aren't very powerful yet. Let's add support for a single boolean operator:

class ClientDB
  def select(query)
    # parse query String
    rules = Hash[ *query.split(/\s*AND\s*/).map do |rule|
      rule.split(/\s*=\s*/).map { |value| value.sub(/\A['"](.+)['"]\z/, '\1') }
    end.flatten ]

    # match records
    @records.select do |record|
      rules.all? { |field, value| record.send(field) == value }
    end
  end
end

The good news? We can now enter queries like:

pp db.select("client_name = 'Gray Soft' AND location = 'Oklahoma'")

But there's a lot of bad news too:

  • My query language hack is weak and easily broken
  • We don't support many great operators like OR and !=
  • We still have no access to the projects field

It's clear that what we really need here is a complete language. But wait, aren't we already using a language? What about Ruby?

This is the line of thinking that leads to Ruby's code blocks. Any method in Ruby can have a bit of code included with the call. That method can then yield to that bit of code, pass it parameters, and even get a return value from it. Let's rework select() again, but this time to use Ruby's code blocks:

class ClientDB
  def select
    @records.select { |record| block_given? && yield(record) }
  end
end

In my experience, you know you're doing Ruby right when you are dropping code and gaining functionality. So how did we do? Check out these queries:

pp db.select { |record| record.client_name != "Gray Soft" }
pp db.select { |record| record.client_name =~ /crew/i }
pp db.select { |record| record.projects.size == 1 }
pp db.select { |record| record.projects.include?("Ruby Quiz") }
# and much, much more...

As you can see, we now have a full language at our command. We have a complete range of operators, access to goodies like Regular Expressions, and we can easily query the Array sub field in any way that we need.

Lambda

All this block stuff is very handy, as you can see. Soon after you start using them, you're going to think, "Now if I could just stick this block in a variable..."

Enter lambda().

Tools like block_given?() and yield() can only be used inside the method we passed the block to. Sometimes we want to hold onto a block object though and pass it around a little:

class ClientDB
  def count(counter)
    counter.call(@records.size)  # call some passed in code block object
  end
end

count = 0
counter = lambda { |items| count += items }
db.count(counter)
pp count  # => 3

In this example, lambda() will just wrap up the provided block in an object you can pass around and use as needed. I've only used it to count a single database here, but I could have easily counted more if we had them.

This example shows off one more interesting aspect of Ruby's blocks: They are closures. Uh oh, ugly Computer Science term alert! It's not as complicated as it sounds. Relax.

See how the lambda() makes use of the local variable? That variable gets updated, even though the lambda() object may get passed away and run who knows where. That's what it means to be a closure. Ruby's blocks "close-up" the binding of where they are created and take it with them. That means they can use local variables and the current value of self.

Putting it all Together

I bet you're wondering what all this has to do with our database example. Let me tell you...

Ruby has one more shortcut for blocks. You can automatically have them wrapped up into an object (or even unwrapped!) at the time of the method call. In other words, I could have written select() like this:

class ClientDB
  def select(&query)
    @records.select { |record| query && query.call(record) }
  end
end

That funny & symbol on the last variable of a method's parameter list tells Ruby, "Just pretend I had wrapped that block with lamdba(), okay?" Ruby will wrap it up and stick it in the variable for you.

Now that didn't change anything for our select() routine, but let's try using the same knowledge in a different way:

class ClientDB
  include Enumerable  # use a mix-in to get select(), map(), to_a(), ...
  def each(&block)    # Enumerable requires each()
    @records.each(&block)
  end
end

I'm only using one new trick here: In each(), I delegate to the each() method of @records by unwrapping the block. The effect of that is the same as if @records.each() had been called directly and passed the block.

Since we defined an each(), we can mix-in Enumerable (a future article topic?) to get a whole slew of new options! Watch this:

pp db.inject(0) { |sum, record| sum + record.projects.size }
pp db.map { |record| record.client_name }
pp db.find { |record| record.client_name == "Gray Soft" }
# and much, much more...

That ends my tour of Ruby's blocks. Remember, the easiest way to let all this sink in is to tell yourself that code is just another data type in Ruby. Don't be too surprised if that gets you solving problems in a whole new way...

Comments (4)
  1. Gregory Brown
    Gregory Brown January 8th, 2006 Reply Link

    In my experience, you know you're doing
    Ruby right when you are dropping code
    and gaining functionality

    This is almost exactly the words I used to describe the refactoring process in Ruby to my employer.

    Great article, James! :)

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
  2. Daniel Berger
    Daniel Berger February 5th, 2006 Reply Link

    Looks like you have the beginnings of a mock database system here. You know, we could really use that for the DBI refactoring effort.

    HINT HINT

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
    2. James Edward Gray II
      James Edward Gray II February 6th, 2006 Reply Link

      Daniel: See KirbyBase for a much more complete example.

      1. Reply (using GitHub Flavored Markdown)

        Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

        Ajax loader
  3. Krešimir Bojčić
    Krešimir Bojčić December 14th, 2011 Reply Link

    Hey,

    I am here 5 years late... It looks like I have a lot of reading to do. Great explanation of the blocks btw.

    1. Reply (using GitHub Flavored Markdown)

      Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

      Ajax loader
Leave a Comment (using GitHub Flavored Markdown)

Comments on this blog are moderated. Spam is removed, formatting is fixed, and there's a zero tolerance policy on intolerance.

Ajax loader