O que mudou no Ruby 2.0


Leia em 11 minutos

Chegou a hora de darmos uma olhada nas principais mudanças introduzidas pelo Ruby 2.0. Se você usa Ruby 1.9, não terá muito o que fazer e a migração será bastante tranquila.

Se você ainda está preso no Ruby 1.8, a história é muito diferente. À partir de Junho de 2013 ele deixará de ter atualizações. E isso inclui atualizações de segurança. Por isso, meu conselho é que você comece a fazer a migração o mais rápido possível.

Neste artigo irei mostrar as principais mudanças introduzidas pela nova versão, lançada no dia 24/02/2013.

Trivia: Você sabia que o lançamento do Ruby 2.0 foi exatamente no dia em que o Ruby completou seu vigésimo aniversário?

Argumentos nomeados

Nós já estamos acostumados a usar hashes quando precisamos passar muitos argumentos para um método. Assim, não temos que nos preocupar com sua ordem e obrigatoriedade.

Agora você pode usar argumentos nomeados, que na assinatura do método se parecem muito com a definição de hashes.

Veja o método abaixo. Ele aceita um único hash com diversas chaves. Se você quiser extrair seus valores em variáveis, é preciso fazer isso manualmente.

def link(options = {})
  href = options[:href]
  text = options[:text]

  puts "href: #{href}"
  puts "text: #{text}"
end

No Ruby 2.0 isso não é mais necessário. Basta definir seus argumentos com o valor de inicialização e a variável será definida automaticamente.

def link(href: nil, text: nil)
  puts "href: #{href}"
  puts "text: #{text}"
end

Se você quiser, pode executar o método com os argumentos em ordem arbitrária.

link href: "http://simplesideias.com.br", text: "SimplesIdeias"
link text: "Hellobits", href: "http://hellobits.com"

Mas o que acontece se passarmos argumentos que não foram definidos?

link rel: "external", text: "Codeplane", href: "http://codeplane.com"
#=> ArgumentError: unknown keyword: rel

Neste caso, uma exceção ArgumentError será lançada, informando qual argumento não foi definido. Se você quiser, pode pegar uma referência para todos os argumentos que forem passados mas não definidos na assinatura do método. Basta usar o double splat.

def link(href: nil, text: nil, **keywords)
  puts "href: #{href}"
  puts "text: #{text}"
end

Todo e qualquer argumento nomeado que não for definido na assinatura do método será armazenado em um hash, que aqui foi chamado de keywords.

Como fica, então, a assinatura do método se eu tiver parâmetros ordenados misturados com parâmetros nomeados?

def catch_all(*args, **keywords)
  p args
  p keywords
end

catch_all "WOW!", "Ruby 2.0 rules!", message: "It rules indeed!"
#=> ["WOW!", "Ruby 2.0 rules!"]
#=> {:message=>"It rules indeed!"}

Note que não é possível mudar a posição do double splat. Se você quiser mesclar argumentos ordenados e nomeados, passe primeiro os ordenados e depois os nomeados.

Uma outra restrição dos argumentos nomeados é que você não pode ignorar os argumentos não definidos sem passar uma variável. Com argumentos ordenados você pode simplesmente usar o *.

def catch_args(*)
end

No caso de argumentos nomeados, ainda não é possível definir apenas **, mas isso já foi corrigido e deve entrar no próximo release.

def catch_keywords(**)
end

catch_keywords a: 1
#=> ArgumentError: unknown keyword: a

O método Method#parameters identifica argumentos nomeados com o símbolo :key e :keyrest.

def inspect_parameters(a, b = nil, *args, c: nil, **keywords, &block)
end

p method(:inspect_parameters).parameters
#=> [
#     [:req, :a],
#     [:opt, :b],
#     [:rest, :args],
#     [:key, :c],
#     [:keyrest, :keywords],
#     [:block, :block]
#   ]

Arrays de símbolos

O Ruby já possuia muitas construções literais para arrays, strings e expressões regulares, como %w[], %() e %r[]. O Ruby 2.0 adicionou mais uma construção, que agora permite criar arrays de símbolos.

platforms = %i[mac windows linux]
[:mac, :windows, :linux]
#=>

O literal %i não permite interpolar valores. É aí que entra o %I.

%i[#{Time.now}]
#=> [:"\#{Time.now}"]

%I[#{Time.now}]
#=> [:"2013-02-26 19:13:13 -0300"]

Encoding

No Ruby 1.9, o encoding padrão é US-ASCII. Isto significa que se você quiser usar caracteres acentuados, por exemplo, precisa definir o magic comment ou uma exceção será lançada.

puts __ENCODING__
#=> US-ASCII

text = "áéíóú"
puts text
#=> invalid multibyte char (US-ASCII)

Agora, o encoding padrão é UTF-8. Você não precisa mais adicionar o magic comment, há menos que queira manter a compatibilidade com o Ruby 1.9, o que é uma boa ideia. Veja este mesmo código ao ser executado no Ruby 2.0:

puts __ENCODING__
#=> UTF-8

text = "áéíóú"
puts text
#=> áéíóú

Module#prepend

Você já deve ter sentido a necessidade de estender o comportamento de um método. Imagine que você tem uma classe chamada MyClass, que possui um único método MyClass#foo. Você precisa estender este método, executando algo antes e depois da implementação original.

class MyClass
  def foo
    puts "foo"
  end
end

Você tem duas alternativas para resolver este problema. A primeira usa os métodos Module.define_method e Module.instance_method e é a mais recomendada, já que não causa efeitos colaterais.

class MyClass
  foo_method = instance_method(:foo)

  define_method :foo do
    puts "before foo"
    result = foo_method.bind(self).call
    puts "after foo"
    result
  end
end

MyClass.new.foo
#=> before foo
#=> original
#=> after foo

A segunda alternativa, muito comum diga-se de passagem, seria renomear o método e executá-lo com este novo nome. O problema acontece se alguém teve a mesma ideia que você e também renomeou o método para MyClass#foo_original.

class MyClass
  alias_method :foo_original, :foo

  def foo
    puts "before foo"
    result = foo_original
    puts "after foo"
    result
  end
end

MyClass.new.foo
#=> before foo
#=> original
#=> after foo

Se você está no Rails, é possível usar o método Module#alias_method_chain, que segue a mesma ideia do Module#alias_method.

require "active_support/all"

class MyClass
  def foo_with_puts
    puts "before foo"
    result = foo_without_puts
    puts "after foo"
    result
  end

  alias_method_chain :foo, :puts
end

MyClass.new.foo
#=> before foo
#=> original
#=> after foo

Com o método Module#prepend isso se torna muito mais simples. Ele permite alterar a hierarquia de classes e módulos de modo que este módulo seja adicionado antes da própria classe. Assim, podemos trocar todas as soluções anteriores por esta, que usa o super.

module FooExtension
  def foo
    puts "before foo"
    result = super
    puts "after foo"
    result
  end
end

class MyClass
  prepend FooExtension
end

MyClass.new.foo
#=> before foo
#=> original
#=> after foo

Mas como isso funciona? Se você executar o método Module.ancestors, verá que o módulo FooExtension foi adicionado na hierarquia antes mesmo da própria classe MyClass.

MyClass.ancestors
#=> [FooExtension, MyClass, Object, Kernel, BasicObject]

Por isso é possível chamar o super, que por sua vez irá executar o método MyClass#foo.

Protocolo to_h

Finalmente temos um protocolo oficial para converter objetos em uma representação em Hash. Trata-se do método to_h, que agora é implementado em várias classes do Core como NilClass, Struct e OpenStruct.

Veja, por exemplo, como fica isso com a classe OpenStruct:

require "ostruct"

user = OpenStruct.new(:name => "John Doe", :email => "john@example.org")

# Ruby 1.9
user.to_h
#=> nil

# Ruby 2.0
#=> {:name => "John Doe", :email => "john@example.org"}

Com a classe Struct o comportamento é semelhante.

User = Struct.new(:name, :email)
user = User.new("John Doe", "john@example.org")

user.to_h
#=> {:name=>"John Doe", :email=>"john@example.org"}

No caso de nil, um Hash vazio é retornado.

nil.to_h
#=> {}

No Ruby 1.9 os métodos NilClass#to_h e Struct#to_h não existem.

Expressões Regulares

O Ruby 2.0 mudou seu engine de expressão regular mais uma vez. O Oniguruma saiu para dar lugar ao Onigmo, um fork do Oniguruma com novas funcionalidades.

Uma das novidades introduzidas pelo Onigmo é a possibilidade de se ter condicionais na expressão regular. Também foram adicionadas algumas funcionalidades do Perl 5.10+.

Para saber quais sintaxes são aceitas no Ruby, acesse a documentação do Onigmo.

Refinements

Uma das grandes preocupações de quem começa a usar Ruby é em relação às classes abertas. A justificativa quase sempre envolve o perigo que existe em modificar classes existentes. Na prática, isso quase nunca acontece, embora vez ou outra você vai dar de frente com um código que faz algo muito bizarro (e que não deveria), mais por culpa do desenvolvedor meia-boca que do Ruby.

Tentando amenizar este problema, o Ruby 2.0 introduziu o Refinements, que é uma maneira de fazermos Monkey Patching de modo mais controlado.

Para mostrar como o Refinements funciona, vamos estender a classe Integer com o método Integer#minutes, que retorna a quantidade de segundos. Você poderia adicionar este método diretamente à classe Integer.

class Integer
  def minutes
    self * 60
  end
end

class MyLib
  puts 3.minutes
  #=> 180
end

puts 5.minutes
#=> 300

É exatamente isso que o ActiveSupport faz, estendendo classes do Core como Time, Date, Hash, Array, dentre outras. O questão é que essas extensões são aplicadas em todo o Ruby.

Para resolver este problema, o Ruby 2.0 adicionou os métodos Module.refine e Module.using. A ideia é adicionar este tipo de extensão de um modo mais controlado. A proposta inicial permitia fazer isso.

module TimeExtension
  refine Integer do
    def minutes
      self * minutes
    end
  end
end

class MyLib
  using TimeExtension

  puts 3.minutes
  #=> 180
end

puts 5.minutes
#=>  undefined method `minutes' for 5:Fixnum (NoMethodError)

Infelizmente a implementação presente no Ruby 2.0 é experimental e reduzida. Este exemplo acima não funciona no Ruby 2.0.0-p0, mas funciona no Ruby 2.0.0-preview1. Em vez disso, você precisa fazer a chamada do método using() no contexto principal. As extensões aplicadas por este serão aplicadas apenas ao arquivo que fez a chamada.

# file: time_extension.rb
module TimeExtension
  refine Integer do
    def minutes
      self * 60
    end
  end
end

# file: with_refinement.rb
require_relative "time_extension"
using TimeExtension

puts 5.minutes
#=>  300

# file: without_refinement.rb
require_relative "time_extension"

puts 5.minutes
#=> undefined method `minutes' for 10:Fixnum (NoMethodError)

Existem muitas incertezas quanto ao modo como o Refinements deve funcionar. O Charles Nutter escreveu um ótimo artigo falando sobre a complexidade e problemas que o Refinements introduz.

Warnings

o Ruby gerará um warning toda vez que você executar um programa com o flag -w se você declarar uma variável e não usá-la.

def parse_name(full_name)
  first, last = full_name.split(" ")
  first
end

parse_name("John Doe")

Ao executarmos esse código:

$ ruby -c -w unused_var.rb
unused_var.rb:2: warning: assigned but unused variable - last
Syntax OK

No Ruby 2.0, variáveis começadas com _ não irão mais gerar estes warnings, mesmo quando não utilizadas.

def parse_name(full_name)
  first, _last = full_name.split(" ")
  first
end

parse_name("John Doe")

E ao verificar a sintaxe:

$ ruby -c -w unused_var.rb
Syntax OK

Constantes

Imagine que você tem uma constante A::B e quer pegar uma referência através do seu nome. Se você utilizar o método Object.const_get, verá que uma exceção será lançada.

module A
  module B
  end
end

Object.const_get("A::B")
#=> NameError: wrong constant name A::B

Para pegar a constante A::B você tinha iterar cada parte do nome da constante.

"A::B"
  .split("::")
  .reduce(Object) {|scope, name| scope.const_get(name) }
#=> A::B

Agora, isso ficou muito mais simples e funciona exatamente do jeito que você espera:

Object.const_get("A::B")
#=> A::B

Enumerator#lazy

O Ruby 2.0 introduziu um modo de criarmos enumerators que podem ser executados em conjuntos grandes ou mesmo infinitos. Veja o intervalo abaixo, por exemplo.

range = (1..Float::INFINITY)

Imagine que você quer pegar os 10 primeiros números primos maiores que 50. Com o método Enumerator#lazy, o estilo de programação não muda nada.

require "prime"

range = (0..Float::INFINITY)

primes = range.lazy
  .reject {|number| number < 50 }
  .select(&:prime?)
  .first(10)

primes
#=> [53, 59, 61, 67, 71, 73, 79, 83, 89, 97]

Se você tentar executar isso sem o método Enumerator#lazy, prepare-se para usar o Ctrl+C. Isso porque o Ruby tentará criar um objeto contendo o intervalo de 0 a infinito, o que obviamente não irá funcionar.

Já com o método Enumerator#lazy, apenas os elementos solicitados é que serão gerados, o que no nosso caso é bastante rápido de fazer, porque estamos acessando apenas 97 números.

Você não precisa ser tão radical quanto ao tamanho da coleção. Mesmo em uma lista muito menor, é possível ver a diferença. Vamos fazer um benchmark com este mesmo exemplo, mas desta vez usando uma coleção com apenas 1000 números.

require "benchmark"
require "prime"

range = (0..1000)
cycles = 1_000

def primes(range)
  range
    .reject {|number| number < 50 }
    .select(&:prime?)
    .first(10)
end

Benchmark.bmbm do |x|
  x.report("with lazy") do
    cycles.times{ primes(range.lazy) }
  end

  x.report("without lazy") do
    cycles.times{ primes(range) }
  end
end

O diferença é muito grande!

Rehearsal ------------------------------------------------
with lazy      0.250000   0.000000   0.250000 (  0.243124)
without lazy   4.800000   0.000000   4.800000 (  4.808169)
--------------------------------------- total: 5.050000sec

                   user     system      total        real
with lazy      0.290000   0.000000   0.290000 (  0.290303)
without lazy   4.250000   0.000000   4.250000 (  4.244091)

Já em coleções menores, usar o Enumerator#lazy pode não ser uma boa ideia. Mudando a quantidade de números gerados para 0..100, temos um resultado um pouco diferente.

Rehearsal ------------------------------------------------
with lazy      0.260000   0.000000   0.260000 (  0.260372)
without lazy   0.220000   0.010000   0.230000 (  0.226624)
--------------------------------------- total: 0.490000sec

                   user     system      total        real
with lazy      0.260000   0.000000   0.260000 (  0.262915)
without lazy   0.200000   0.000000   0.200000 (  0.200987)

TracePoint

O Ruby possui o método Kernel#set_trace_func, que permite rastrear diversos eventos durante a execução do código. Ele aceita um bloco, que pode receber até seis argumentos.

set_trace_func proc {|event, file, line, method, binding, class_name|
  puts "%8s %s:%-2d %10s %8s" % [event, file, line, method, class_name]
}

class Foo
  def initialize(options = {})
    @options = options
  end
end

Ao executar este arquivo, teremos uma saída como esta:

$ ruby trace.rb
  c-return trace.rb:1   set_trace_func     Kernel
      line trace.rb:5
    c-call trace.rb:5        inherited      Class
  c-return trace.rb:5        inherited      Class
     class trace.rb:5
      line trace.rb:6
    c-call trace.rb:6     method_added     Module
  c-return trace.rb:6     method_added     Module
       end trace.rb:9

Você pode ouvir até oito eventos diferentes:

O Ruby 2.0 introduziu uma nova classe chamada TracePoint, que permite utilizar a mesma funcionalidade do método Kernel#set_trace_func usando uma API orientada a objetos.

Usando a classe TracePoint, o exemplo acima seria definido assim:

trace = TracePoint.new do |t|
  puts "%10s %s:%-2d %15s %10s" % [t.event, t.path, t.lineno, t.method_id, t.defined_class]
end

A saída é um pouco diferente, pois o nome do evento possui _ em vez de -.

$ ruby trace.rb
  c_return trace.rb:9           enable TracePoint
      line trace.rb:11
    c_call trace.rb:11       inherited      Class
  c_return trace.rb:11       inherited      Class
     class trace.rb:11
      line trace.rb:12
    c_call trace.rb:12    method_added     Module
  c_return trace.rb:12    method_added     Module
       end trace.rb:15

Se você quiser, pode ouvir um evento específico. Para isso, basta passar seu nome para o initializer.

trace = TracePoint.new(:class, &block)

Diretório corrente de um arquivo

Agora é possível pegar uma referência do diretório atual de um script que está sendo executado com o método Kernel#__dir__.

puts __FILE__
#=> ruby20_dir.rb

puts __dir__
#=> /Projects/samples

O método Kernel#__dir__ é equivalente a:

File.dirname(File.realpath(__FILE__))

Note que ele é todo em minúsculas, ao contrário de __FILE__. Isso acontece porque __FILE__ é apenas uma expressão, enquanto Kernel#__dir__ é um método. Não que isso seja uma justificativa, já que podemos criar um alias para este método, adicionando uma versão toda em maiúsculas.

module Kernel
  alias_method :__DIR__, :__dir__
end

puts __dir__
#=> /Projects/samples

puts __DIR__
#=> /Projects/samples

DTrace

O Ruby 2.0 trouxe integração com o DTrace, uma ferramenta que permite fazer a análise de performance e errors do sistema operacional e programas executados. O probes reconhecidos pelo Ruby estão disponíveis no wiki do projeto.

Outras mudanças

Finalizando

Como você pode perceber, o Ruby 2.0 tem muitas novidades. Eu não listei aqui tudo o que tem novo. Para saber exatamente o que entrou nesta versão, acesse a documentação.

Tenho que dizer que estou muito empolgado com esta versão! Se o Rails 4 der uma forcinha e exigir o Ruby 2.0, seria excelente! E isso não é nada impossível, já que em todos os projetos que testei o Ruby 2.0, nenhum precisou de mudanças para rodar o Rails 3.2.