A Look at Ruby 2.0

Tweet

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

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Erik

    Great writeup Thiago!

    I noticed in the 4th bit of example code for keyword arguments you have:

    > config(timeout: 10, enabled: false) #inverse order
    #=> [false, 20]

    It should be
    #=> [false, 10]

    right?

    • https://github.com/tjackiw Thiago Jackiw

      Ha! Nice catch. Thanks Erik ;)

  • http://postmodern.github.com/ Postmodern

    You should mention that Debian and RedHat systems also provide libyaml packages:

    apt-get install libyaml-dev

    yum install libyaml-devel

  • Dan Bruns

    In testing Edge Rails (4) with Ruby 2.0 RC1 using rvm, I had a little trouble with complaints about having to compile ruby with openssl support. I followed the instructions on https://rvm.io/packages/openssl to get things working, but the only one that actually worked was rvm pkg install openssl and recompiling referencing the $rvm_path/usr directory.

  • Arno.Nyhm

    Its still possible with named parameters to give a hash as one parameter? (Combination of the old and new style?)

  • sowcow

    I started gem that use refinements, its like mini rspec without .shoulds
    https://github.com/sowcow/testdo/blob/master/features/testdo.feature