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 = trueActually 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