29 maja 2012

Blocks in Ruby

One of the first things that a new Ruby programmer finds interesting in this language are blocks. On first sight they look a little bit weird. However after a first few uses they become as natural as breathing. Ruby blocks are very simple and in the same time very powerful mechanism. That's why I made up my mind to write a short article about them.

I like to think about blocks as anonymous functions. We can define them right after a method invocation into which we want to pass them. Blocks must be enclosed between do - end keywords or between curly brackets. At the beginning of a block we can specify a list of parameters. They should be placed inside | | sign. After that we have a function body. Example of a Ruby block you can see below.
file_sandwich(file_name) do |file|
   count = 0
   while line = file.gets
      count += 1
   end
   count
end
We can replace do - end keywords with curly brackets and it will works as well.
file_sandwich(file_name) { |file|
   count = 0
   while line = file.gets
      count += 1
   end
   count
}
Now that we have defined block let's see how does it work inside our file_sandwich method.
def file_sandwich(file_name)
   file = open(file_name)
   yield(file) if block_given?
ensure
   file.close if file
end
The most important for us is the third line. block_given? is a function that checks if we pass a block. yield keyword execute a passed block. As you can see I pass file variable into yield invocation as an argument. Thats because I have specified that our block has a one parameter.

Ok, but what if we want to pass more then one block ? Well, after every method we can define only one block but that doesn't mean we cannot pass more blocks. To do that first we need to know what is a class of block object. So we need to pass our block to a function in a more explicit way and check it classes. When we define function parameter with name that starts with & sign then Ruby will know that it should bind this parameter with passed block. We will use this parameter to get information about its class. So lets do that.
def file_sandwich(file_name, &block)
   puts block.class # => Proc
   file = open(file_name)
   block.call if block
ensure
   file.close if file
end
So blocks are Proc objects ! It means we can create Proc objects and pass them to function as arguments.

Now that we know a bit about blocks you are probably wondering where we can use them. Well, when programming in Ruby they are quite helpful when dealing with :
  • event handling
  • resource management
  • iteration
  • concurrency
  • synchronization
  • and much more...
For me one of the most awesome possibilities that blocks give us is to create very flexible and DSL-like interfaces. Lets assume that we have a Splitter class that is responsible for splitting a string object into an array of strings. We can define this class in a normal Java-like way and it will look like this:
splitter = Splitter.new

splitter.separator = ";"
splitter.trim_result = true
splitter.omit_empty_string = true
Actually nothing special, right ? Ok, but what if we are able to provide a possibility to create and configure Splitter object in such a way:
Splitter.create do
   separator         ";"
   trim_result       true
   omit_empty_string true
end
Implementation of such an interface isn't very hard. Actually I did it using only factory method pattern, alias method mechanism and instance_eval method. You can find my implementation below. I hope I have explaind a little bit idea of how blocks work and how they can be used.
class Splitter

  # more code here...

  def separator(value=Nil)
    return @separator unless value
    @separator=value
  end
  
  def trim_result(value=Nil)
    return @trim_result unless value
    @trim_result=value
  end
  
  def omit_empty_string(value=Nil)
    return @omit_empty_string unless value
    @omit_empty_string=value
  end

  def self.create (&block)

    splitter = Splitter.new
    splitter.instance_eval(&block)
    splitter
  end
  
  alias_method :separator=, :separator
  alias_method :trim_result=, :trim_result
  alias_method :omit_empty_string=, :omit_empty_string
end

Brak komentarzy:

Prześlij komentarz