A Look at Ruby 2.0By Thiago Jackiw
With Ruby 2.0 set to be released on February 24th, exactly on the 20th anniversary of Ruby’s first debut, I decided to write this article to give you a quick rundown of some of the most interesting changes. And if you would like to experiment with this version before the official release is out, you can do so by following the instructions in this article.
Installing RC1
Ruby 2.0 has deprecated the use of syck in favor of psych, and YAML is now completely dependent on libyaml. This means that we must install this library before installing Ruby 2.0.
On any *nix based system, we can install it by downloading the source package and manually building it:
$ wget $ tar xzvf yaml-0.1.4.tar.gz $ cd yaml-0.1.4 $ ./configure $ make $ make install
Or if you are on a Mac, you can install it with homebrew:
$ brew update $ brew install libyaml
Once libyaml is installed, we can go ahead and install RC1 using rvm:
$ rvm install ruby-2.0.0-rc1
Next, we need to tell rvm to use this new Ruby version:
$ rvm use ruby-2.0.0-rc1
Great, we are now ready to dive into the changes.
Changes
The changes introduced in 2.0 are pretty extensive, but in this article I will be focusing mainly on the following:
- Refinements
- Keyword Arguments
- Module#prepend
- Enumerable#lazy
- Language Changes
Refinements (Experimental)
Refinements are set to replace unsafe monkey-patching by providing a better, safer, and isolated way to patch code. Traditionally, when a patch is applied, it modifies the object globally – whether you like it or not. With refinements, you can limit monkey-patching to certain scopes.
Let us look at a concrete monkey-patching example. Say we wanted to extend the String class and add a bang method that adds an exclamation point after the given string. We would do something like this:
class String def bang "#{self}!" end end
This change is now global. Which means that any string that calls
.bang will behave as such:
> "hello".bang #=> "hello!"
To prevent this global scope change, refinement was proposed. It works by utilizing two new methods:
Module#refine and
main.using. The first, is a block that allows for locally-scoped monkey patching. And the latter, imports refinements into the current file or eval string, so that it can be used in other places.
Taking our previous example, this is how we can safely extend the String class using refinements:
module StringBang refine String do def bang "#{self}!" end end end
Now, if we try to call
.bang on any string, it will fail:
> "hello".bang #=> NoMethodError: undefined method `bang' for "":String
This is because the change to the String class is contained within the StringBang module. After we import this refinement with the
using keyword, it works as expected:
> using StringBang > "hello".bang #=> "hello!"
WARNING: This feature is still experimental and the behavior may change in future versions of Ruby. Charles Nutter of JRuby has a great explanation of the challenges presented with this.
Keyword Arguments
Also known as named parameters, this feature is pretty useful, as it allows a method to be declared to receive keyword arguments. This is used by many languages, and it is finally integrated into Ruby.
In the old way, if you wanted to accept keyword arguments, you would have to fake it by receiving a hash argument in a method:
def config(opts={}); end
And if you had default values, you would then merge the user-supplied arguments on top of your default values:
def config(opts={}) defaults = {enabled: true, timeout: 300} opts = defaults.merge(opts) end
It sure works but it is a hack, and I am sure we have all used it in one way or another.
Forget this old way and say hello to the true keyword arguments in Ruby 2.0. Using the same example as above, here is how we can define our
config method in the new way:
def config(enabled: true, timeout: 300) [enabled, timeout] end
Now, let us see the different ways in which we can invoke this method:
> config() #no args #=> [true, 300] > config(enabled: false) #only enabled #=> [false, 300] > config(timeout: 20) #only timeout #=> [true, 20] > config(timeout: 10, enabled: false) #inverse order #=> [false, 10]
Extending the
config method further, we are now going to accept two extra arguments:
value, a required value; and
other, an optional hash.
def config(value, enabled: true, timeout: 300, **other) [value, enabled, timeout, **other] end
And here are the different that we can interact this method:
> config() #no args #=> ArgumentError: wrong number of arguments (0 for 1) > config(1) #required value #=> [1, true, 300, {}] > config(1, other: false) #required value, optional hash #=> [1, true, 300, {:other=>false}] > config(1, timeout: 10) #required value, timeout #=> [1, true, 10, {}] > config(1, timeout: 10, other: false) #required value, timeout, optional hash #=> [1, true, 10, {:other=>false}] > config(1, other: false, another: true, timeout: 10, enabled: false) #inverse order #=> [1, false, 10, {:other=>false, :another=>true}]
Module#prepend
This is similar to
Module#include except that it is set to “overlay the constants, methods, and module variables” (Source) of the prepending module. To better understand this concept, let us look at how
Module#include currently works:
module Foo def baz 'foo-baz' end end class Bar include Foo def baz 'bar-baz' end end
Here we are declaring a module and a class, and each contain a declaration of the
baz method. When we invoke the
baz method in the Bar class, the method declared in the Foo module is ignored:
> Bar.new.baz #=> "bar-baz"
With
Module#prepend, it is the inverse; the declaration in the module overwrites the declaration in the class. Rewriting the example above to use
Module#prepend, here is the new code:
module Foo def baz 'foo-baz' end end class Bar prepend Foo def baz 'bar-baz' end end
And when invoking the
baz method in the Bar class, the method in the Foo module is the one that actually gets called:
> Bar.new.baz #=> "foo-baz"
Enumerable#lazy
Quoted directly from the documentation:
Returns a lazy enumerator, whose methods map/collect, flatmap/collectconcat, select/findall, reject, grep, zip, take, #takewhile, drop, #drop_while, and cycle enumerate values only on an as-needed basis. However, if a block is given to zip or cycle, values are enumerated immediately.”
Now that we have an understanding of its concept, let us look at an example:
> ary = [1,2,3,4,5].select{|n| n > 2} #=> [3, 4, 5] > ary = [1,2,3,4,5].lazy.select{|n| n > 2} #=> #:select> > ary.force #=> [3, 4, 5]
In the first part, without using the
lazy method, a new array is returned immediately after the
select method completes the evaluation. In the second part, when the
lazy method is used, a lazy enumerator is returned and the code does not get evaluated until we call
force (or
to_a).
Language Changes
You can now use %i and %I for symbol list creation:
> %i{this is a list of symbols} #=> [:this, :is, :a, :list, :of, :symbols]
Conclusion
This article presented a few of the most talked about changes included in Ruby 2.0, and I personally cannot wait for the official release to be out.
If you are still using Ruby 1.8x, it is strongly advisable to upgrade to 1.9.3 as soon as possible, as it will soon be deprecated and no longer maintained. As for the compatibility between Ruby 2.0 and 1.9.3, it is said to be fully compatible.