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 http://pyyaml.org/download/libyaml/yaml-0.1.4.tar.gz
$ 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.
Resources
- rvm
- homebrew
- syck
- psych
- libyaml
- Refinements
- Module#prepend
- Enumerable#lazy
Thiago Jackiw is a well-rounded Engineering Manager / Senior Software Engineer and Entrepreneur who enjoys exploring new technologies. He has been working with Ruby since 2006 and currently lives in the San Francisco Bay Area. You should follow him on Twitter, and check his latest open source projects on GitHub.