There are a couple of ways to initialize an object in Ruby. Oftentimes, this is under the guise of applying configuration on an object. They usually mean the same thing. Here’s an example taken from the Twitter Ruby Gem:[4]
| client = Twitter::REST::Client.new do |config| |
| config.consumer_key = "YOUR_CONSUMER_KEY" |
| config.consumer_secret = "YOUR_CONSUMER_SECRET" |
| config.access_token = "YOUR_ACCESS_TOKEN" |
| config.access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
See if you can guess at how this is implemented. Here’s a hint: consumer_key and the rest belong to accessors of Twitter::REST::Client. You’re going to build the bare minimum to get the preceding code to work. The code is doing two things:
The object instantiation bit is trivial. What’s interesting here is that the initializer of Twitter::REST::Client accepts a block. This block takes a single argument called config. Within the block body, the fields of the config object are set using various values.
Create a file called object_init.rb. You will need to set up the modules and class for Twitter::REST::Client:
| module Twitter |
| module REST |
| class Client |
| end |
| end |
| end |
| |
| client = Twitter::REST::Client.new do |config| |
| config.consumer_key = "YOUR_CONSUMER_KEY" |
| config.consumer_secret = "YOUR_CONSUMER_SECRET" |
| config.access_token = "YOUR_ACCESS_TOKEN" |
| config.access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
If you try to run this code, you won’t get any errors. Why? Because Ruby ignores the block when it’s not called within the method body, since there is no yield (yet). Let’s make that block do something. We know from looking at it that:
It is being called from the initializer.
It accepts one single argument, the config object.
The config object has a couple of setters, such as consumer_key.
The main thing to realize is that the config object can be the same instance created by Twitter::REST::Client.new. Why the can? The short answer is that you can make things a bit more complicated by passing in a configuration object, but let’s stick to simple. Therefore, you now know one additional thing:
config and the instantiated object can be the same thing. Now you should have a better idea:
| module Twitter |
| module REST |
| class Client |
» | attr_accessor :consumer_key, :consumer_secret, |
» | :access_token, :access_token_secret |
» | |
» | def initialize |
» | yield self |
» | end |
| end |
| end |
| end |
| |
| client = Twitter::REST::Client.new do |config| |
| config.consumer_key = "YOUR_CONSUMER_KEY" |
| config.consumer_secret = "YOUR_CONSUMER_SECRET" |
| config.access_token = "YOUR_ACCESS_TOKEN" |
| config.access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
In the initializer, self (that is, the Twitter::REST::Client instance) is passed into the block. Within the block body, the instance methods are called. These instance methods were created with attr_accessor. You can now try it out:
| client = Twitter::REST::Client.new do |config| |
| config.consumer_key = "YOUR_CONSUMER_KEY" |
| config.consumer_secret = "YOUR_CONSUMER_SECRET" |
| config.access_token = "YOUR_ACCESS_TOKEN" |
| config.access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
| |
| p client.consumer_key |
What happens if you initialize the client without a block? You’ll get a LocalJumpError complaining that no block is given:
| 'initialize': no block given (yield) (LocalJumpError) |
This is an easy fix. Remember block_given?? You can use it in initialize:
| def initialize |
| yield self if block_given? |
| end |
Run the code again, and this time everything should work as expected. We will revisit this example soon, where we consider an even more flexible approach to initializing the config object. But before that, let’s enter into the world of meta-programming and DSL creation.
In the previous section, you saw one flavor of object initialization using blocks. Here’s another example, adapted and modified from Rails. Let’s imagine that you want to define a bunch of routes in a web framework, such as Rails. Routes are rules that you declare for an incoming web request. These rules invoke the appropriate controller and controller method, depending on the pattern of the URL of the incoming web request. For example, if the web server receives a request with http://localhost:3000/users, a route would parse the incoming request URL and ensure that the index method of the UsersController will be invoked.
In Rails, this file is located in config/routes.rb. In older versions of Rails, the syntax looked like this:
| routes = Router.new do |r| |
| r.match '/about' => 'home#about' |
| r.match '/users' => 'users#index' |
| end |
However, as Rails evolved, the way routes were defined also changed:
| routes = Router.new do |
| match '/about' => 'home#about' |
| match '/users' => 'users#index' |
| end |
In the new syntax, the block no longer expects an argument. With nothing to pass into yield, which object does match belong to? Learning the techniques in creating this variation to object instantiation will also allow you to create DSLs.
Create a file called router.rb, and fill it in with this initial implementation:
| routes = Router.new do |
| match '/about' => 'home#about' |
| match '/users' => 'users#index' |
| end |
| |
| class Router |
| # We are going to implement this! |
| end |
The end goal is to print out the routes:
| {"/about"=>"home#about"} |
| {"/users"=>"users#index"} |
An actual router implementation will parse the string and invoke the appropriate controller and method. For our purpose, printing out the routes will suffice. Your job is to fill in the body of the Router class. One way the task can potentially be simplified is to create an implementation that looks like this:
| routes = Router.new do |r| |
| r.match '/about' => 'home#about' |
| r.match '/users' => 'users#index' |
| end |
This should look familiar to you. Let’s go through building up this class, though this time the pace will be faster.
The initializer of Router takes a block that accepts a single argument. That argument is the object itself. You should also be able to infer that Router has an instance method called match that takes a hash as an argument. Here’s the fastest way to implement this class:
| class Router |
| def initialize |
| yield self |
| end |
| |
| def match(route) |
| puts route |
| end |
| end |
Of course, the match method doesn’t do anything interesting. But that’s not the point. Here’s the challenge: how do you get from
| routes = Router.new do |r| |
| r.match '/about' => 'home#about' |
| r.match '/users' => 'users#index' |
| end |
to this?
| routes = Router.new do |
| match '/about' => 'home#about' |
| match '/users' => 'users#index' |
| end |
To make that leap, you will need to know about instance_eval and some meta-programming gymnastics. Let’s get to that right away.
When a method is called without a receiver (the object that the method is called on), it is assumed that the receiver is self. What does self mean in a block? Open irb and let’s find out. Let’s ask irb the oldest existential question in the world: what is self?
| >> self |
| => main |
| >> self.class |
| => Object |
Recall that in Ruby, everything is an object. This means that when you perform some operation, this is done within the context of some object. In this case, main is an object that belongs to the Object class.
Now, let’s answer the next question. Within the confines of a block, what does self refer to? Try this out in irb:
| >> def foo |
| >> yield self |
| >> end |
| => :foo |
| |
| >> foo do |
| >> puts self |
| >> end |
| main |
| => nil |
In a block, self is in the context where the block was defined. Since the block was defined in the main scope, self refers to the main. This means that doing
| routes = Router.new do |
| match '/about' => 'home#about' |
| end |
will result in an error:
| in `block in <main>': undefined method `match' for main:Object (NoMethodError) |
This is because Ruby is trying to look for the match method defined on main. You need to tell Ruby to evaluate the match method in the context of Router. In less malleable languages, it is extremely difficult to accomplish this. Not with Ruby.
You need to somehow convince Ruby that the self in
| self.match '/about' => 'home#about' |
refers to the Router instance, not the main object. This is exactly what instance_eval is for. instance_eval evaluates code in the context of the instance. In other words, instance_eval changes self to point to the instance you tell it to.
Back to router.rb. Modify initialize to look like this:
| class Router |
| |
| def initialize(&block) |
| # self is the Router instance |
| instance_eval &block |
| end |
| |
| # ... |
| end |
There are some new things with this code. Previously, all our encounters with block invocation were implicit via yield. Here, the code makes explicit that the block should be captured in &block. The reason it needs to be stored in a variable is because the block needs to be passed into instance_eval.
What about the "&" in &block? For that, we need to take a quick detour to learn about block-to-Proc conversion.
We have not covered Procs yet. For now, think of them as lambdas. Blocks are not represented as objects in Ruby. However, instance_eval expects to be given an object. Therefore, we need to somehow turn a block into an object.
In Ruby, this is done via a special syntax: &block. When Ruby sees this, it internally converts the captured block into a Proc object.
The rules for block-to-Proc conversion can be utterly confusing. Here’s a useful way to remember it:
Block → Proc if &block is in a method argument.
Proc → Block if &block is in method body.
Now that you understand block-to-Proc conversion, let’s circle back to where we left off and see how everything is put together. This is the final result:
| class Router |
| |
| def initialize(&block) |
| instance_eval &block |
| end |
| |
| def match(route) |
| puts route |
| end |
| |
| end |
| |
| routes = Router.new do |
| match '/about' => 'home#about' |
| end |
When the Router instance is given a block, it is converted into a Proc, then passed into instance_eval. Since the context where instance_eval is invoked is Router, the Proc object is also evaluated in the Router context. This means that the match method is invoked on the Router instance.
Now that you know how instance_eval and block-to-Proc conversion works, let’s revisit an earlier example on object initialization and apply your newfound knowledge.
Let’s say you want the initializer to be slightly more flexible and take an options hash. Furthermore, you also think that specifying config.consumer_secret is too verbose. In other words, something like this:
| client = Twitter::REST::Client.new({consumer_key: "YOUR_CONSUMER_KEY"}) do |
| consumer_secret = "YOUR_CONSUMER_SECRET" |
| access_token = "YOUR_ACCESS_TOKEN" |
| access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
How could this be implemented? First, the hash of options would need to be iterated through. This would then be followed by calling the block:
» | def initialize(options = {}, &block) |
» | options.each { |k,v| send("#{k}=", v) } |
» | instance_eval(&block) if block_given? |
» | end |
Take note of the extra = in calling send. Because the methods were constructed using attr_accessor, Ruby uses the trailing = to indicate that the method is an attribute writer. Otherwise, it assumes it is an attribute reader.
| module Twitter |
| module REST |
| class Client |
| attr_accessor :consumer_key, :consumer_secret, |
| :access_token, :access_token_secret |
| |
» | def initialize(options = {}, &block) |
» | options.each { |k,v| send("#{k}=", v) } |
» | instance_eval(&block) if block_given? |
» | end |
| end |
| end |
| end |
| |
| client = Twitter::REST::Client.new({consumer_key: "YOUR_CONSUMER_KEY"}) do |
| consumer_secret = "YOUR_CONSUMER_SECRET" |
| access_token = "YOUR_ACCESS_TOKEN" |
| access_token_secret = "YOUR_ACCESS_SECRET" |
| end |
| |
| p client.consumer_key # => YOUR_CONSUMER_KEY |
| p client.access_token # => YOUR_ACCESS_TOKEN |
Now, anyone initializing a Twitter::Rest::Client can choose to do it via an options hash, use a block, or use a combination of both. Now it’s time to flex those programming muscles and work on the exercises.
52.15.38.176