Go to English Blog

O que mudou no Ruby 2.3

Leia em 9 minutos

Como já é tradição no mundo Ruby, provavelmente teremos uma nova versão em breve. Trata-se do Ruby 2.3, que está previsto para ser lançado em 25 de dezembro de 2015.

Neste artigo iremos ver quais são as mudanças mais interessantes que esta nova versão introduz. Para ver uma lista completa de todas as mudanças, acesse a lista de mudanças.

Frozen strings

Até o Ruby 2.2, as strings são mutáveis, ou seja, dada uma string, você pode alterar seu valor sem precisar instanciar um novo objeto.

language = "Ruby"
#=> "Ruby"

language.upcase!
#=> "RUBY"

language
#=> "RUBY"

Para que isso aconteça, o Ruby tem que instanciar um novo objeto toda vez que você define uma string, mesmo que ela já tenha sido criada anteriormente.

"Ruby".object_id
#=> 70168324162560

"Ruby".object_id
#=> 70168315321340

Perceba como os identificadores acima são diferentes. Isso acontece sempre que você definir uma nova string. Em alguns casos, você pode usar uma string imutável, que irá reutilizar este objeto. Quando você precisa de alguma otimização extra, definir uma constante e torná-la imutável irá ajudar:

class Greeting
  DEFAULT_GREETING = "Hello there!".freeze
end

Em alguma situações, você pode ter um ganho de performance1 se reutilizar o mesmo objeto. O exemplo abaixo mostra a quantidade de objetos alocados em um loop.

strings = []

before_count = GC.stat[:total_allocated_objects]

10_000.times { strings << "Ruby" }

GC.stat[:total_allocated_objects] - before_count
#=> 10001

Como você pode ver, foram alocados 10001 objetos neste loop. Com uma simples mudança, o números de objetos alocados poderia cair para 1.

strings = []

before_count = GC.stat[:total_allocated_objects]

10_000.times { strings << "Ruby".freeze }

GC.stat[:total_allocated_objects] - before_count
#=> 1

Muitas bibliotecas utilizam deste artifício para diminuir a quantidade de objetos alocados. Veja, por exemplo, como o Rack faz:

# rack-1.6.4/lib/rack.rb
module Rack
  # ...

  PATH_INFO      = 'PATH_INFO'.freeze
  REQUEST_METHOD = 'REQUEST_METHOD'.freeze
  SCRIPT_NAME    = 'SCRIPT_NAME'.freeze
  QUERY_STRING   = 'QUERY_STRING'.freeze
  CACHE_CONTROL  = 'Cache-Control'.freeze
  CONTENT_LENGTH = 'Content-Length'.freeze
  CONTENT_TYPE   = 'Content-Type'.freeze

  GET  = 'GET'.freeze
  HEAD = 'HEAD'.freeze

  # ...
end

Posteriormente, essas constantes são utilizadas por todo o código, para evitar a alocação de strings que vão servir apenas para acessar hashes. Veja um benchmark simples mostrando a diferença entre strings mutáveis e imutáveis.

require "benchmark/ips"

GC.disable

CYCLES = 10_000_000

Benchmark.ips do |x|
  x.report("immutable") {CYCLES.times{ "Ruby".freeze }}
  x.report("mutable") {CYCLES.times{ "Ruby" }}
  x.compare!
end

# Calculating -------------------------------------
#              mutable     1.000  i/100ms
#            immutable     1.000  i/100ms
# -------------------------------------------------
#              mutable      0.828  (±120.7%) i/s -      3.000  in   6.152329s
#            immutable      1.926  (± 0.0%) i/s  -     10.000 
# 
# Comparison:
#            immutable:        1.9 i/s
#              mutable:        0.8 i/s - 2.33x slower

No Ruby 2.3.0-preview2, strings mutáveis foram ~2x mais lentas que strings imutáveis. No ruby-2.2.3 essa diferença é ainda maior, chegando a ser 4x mais lentas.

Além disso, as alocações de objetos necessárias pelas strings mutáveis precisam sem removidas pelo Garbage Collector com mais frequência, como no exemplo do loop. Veja:

GC.start
1_000_000.times { "a" }
GC.stat[:count]
#=> 48

Se formos usar apenas strings imutáveis, a quantidade de vezes que o Garbage Collector precisará executar é bem menor:

GC.start
1_000_000.times { "a".freeze }
GC.stat[:count]
#=> 5

Pensando nisso, o Ruby trouxe suporte para strings imutáveis definidas no escopo de execução. Atualmente é possível ativar essa opção de modos diferentes; a maneira mais simples é usar o novo flag do interpretador, --enable-frozen-string-literal2.

$ ruby --enable-frozen-string-literal -e 'puts "Ruby".frozen?'
true

Você também pode ativar esta opção com um magic comment por arquivo; isso significa que todas as strings definidas naquele arquivo serão imutáveis.

# frozen_string_literal: true

"Ruby".frozen?
#=> true

Para facilitar a conversão de strings, foram adicionados os métodos String#+@ e String#-@.

(+"Ruby").frozen?
#=> false

(-"Ruby").frozen?
#=> true

O método String#+@ é particularmente útil para os casos onde você tem strings imutáveis por padrão e precisa modificar uma string, pois ele é 2x mais rápido que String#dup.

# frozen_string_literal: true

name = "John"
message = +"Hello there, " << name << "!"
#=> Hello there, John!

Existe a possibilidade de strings imutáveis serem o padrão no Ruby 3.0, sem a necessidade de se configurar nada. Por isso, se você usa e abusa de mutabilidade de strings, está na hora de repensar sua estratégia.

Tickets: #8976, #11725, #11782.

Safe navigation operator

Um novo operador foi adicionado ao Ruby. Trata-se do Safe navigation operator, também conhecido como lonely operator. Ele funciona de forma semelhante ao método Object#try, utilizado principalmente em aplicações Ruby on Rails. Com ele você pode encadear chamadas sem a necessidade de verificar se o objeto tem um valor nulo.

name = nil

# ruby-2.3.0+
name&.upcase
#=> nil

# ActiveSupport
name.try(:upcase)
#=> nil

A maior diferença entre o lonely operator e a implementação adicionada pelo método Object#try é que a versão nativa fará lazy evaluation, executando expressões apenas quando o objeto é um valor diferente de nil.

def run
  puts "=> Executed!"
end

nil&.call(run)
#=> nil

Com o ActiveSupport, o método run seria sempre executado.

def run
  puts "=> Executed!"
end

nil.try(run)
#=> Executed!
#=> nil

Outra diferença importante é que a atribuições também são possíveis com o lonely operator.

class Counter
  attr_accessor :count

  def initialize
    @count = 0
  end
end

counter = Counter.new
counter&.count += 1
#=> 1

Finalmente, vale deixar claro que a implementação do lonely operator é mais parecida com a de Object#try!, que lançará uma exceção NoMethodError caso o método não exista no objeto-alvo quando este é um valor não-nulo.

num = 0
#=> 0

num&.upcase
#=> NoMethodError: undefined method `upcase' for 0:Fixnum

Ticket: #11537

Acessando estruturas encadeadas

Você provavelmente já precisou acessar objetos em uma estrutura encadeada, onde seu acesso parecia muito verboso. E uma preocupação muito comum é garantir que os objetos-pai existam, para evitar que exceções como NoMethodError: undefined method [] for nil:NilClass sejam lançadas. Com os métodos Array#dig e Hash#dig sua vida se tornou muito mais simples, sem a necessidade de usar uma biblioteca externa.

hash = {a: {b: {c: "hello"}}}

hash.dig(:a, :b, :c)
#=> "hello"

hash.dig(:d, :e, :f)
#=> nil

array = [[["hello"]]]

array.dig(0, 0, 0)
#=> "hello"

array.dig(1, 1, 1)
#=> nil

O mais interessante é que você pode, obviamente, usar isso para navegar uma estrutura mais complexa composta por arrays e hashes, como aquelas retornas por APIs. Veja a diferença para pegar o nome do primeiro usuário da estrutura à seguir:

data = {users: [{name: "John Doe"}]}

# ruby-2.2
data[:users] && data[:users][0] && data[:users][0][:name]
#=> "John Doe"

# ruby-2.3
data.dig(:users, 0, :name)
#=> "John Doe"

Vale lembrar que outras classes também implementam o método dig, como Struct e OpenStruct.

Tickets: #11643 e #11688.

Hash#to_proc

A classe Hash agora implementa o método Hash#to_proc, extremamente útil para os casos onde você precisa pegar todos os valores dada uma lista de chaves.

hash = {a: 1, b: 2, c: 3}
wanted_keys = [:a, :c]

# ruby-2.2
wanted_keys.map {|key| hash[key] }
#=> [1, 3]

# ruby-2.3
wanted_keys.map(&hash)
#=> [1, 3]

Para entender como isso funciona, vamos ver como seria uma possível implementação de Hash#to_proc.

class Hash
  def to_proc(&block)
    proc {|key| self[key] }
  end
end

hash = {a: 1}
hash.to_proc.call(:a)
#=> 1
  1. Toda vez que um objeto precisa ser convertido em Proc, o método to_proc é executado. Seria o equivalente a converter o objeto em bloco. Isso é feito implicitamente quando usamos array.map(&hash), por exemplo.
  2. O método que recebe essa Proc irá, por sua vez, executá-la passando cada um dos itens do array como sendo o argumento da execução, como em hash.to_proc.call(:a).
  3. A Proc recebe a chave e retorna o valor associado a ela.

A mesma ideia se aplica na implementação de Symbol#to_proc, ao qual já estamos acostumados. Uma possível implementação pode ser vista à seguir:

class Symbol
  def to_proc(&block)
    proc {|target| target.public_send(self) }
  end
end

:upcase.to_proc.call("hello")
#=> "HELLO"

Ticket: #11653.

Hash#fetch_values

O método Hash#fetch_values retorna o valor de diversas chaves de uma só vez. Caso uma das chaves fornecidas não exista, a exceção KeyError será lançada. Essa é a principal diferença em relação ao já existente Hash#values_at, que retorna nil para chaves não encontradas.

hash = {a: 1, b: 2, c: 3}

hash.values_at(:a, :c)
#=> [1, 3]

hash.fetch_values(:a, :c)
#=> [1, 3]

hash.values_at(:a, :d)
#=> [1, nil]

hash.fetch_values(:a, :d)
#=> KeyError: key not found: :d

Ticket: #10017.

Comparação de hashes

Se você já precisou fazer comparações entre hashes, deve ter tentado usar o método Hash#include?. Imagine que você quer saber se o hash a contém o hash b. Parece natural fazer algo como a.include?(b). Acontece que o método Hash#include? não passa de um alias para o método Hash#has_key? e irá apenas verificar se a chave é existente ou não.

Para resolver este problema, agora podemos usar alguns os métodos Hash#>, Hash#>=, Hash#< e Hash#<= para fazer este tipo de comparação.

# left é exatamente igual a right?
{a: 1} == {a: 1}
#=> true

# left contém e tem mais itens que right?
{a: 1, b: 1} > {a: 1}
#=> true

# left contém right e, opcionalmente, possui mais itens?
{a: 1} >= {a: 1}
#=> true

# left contém right e possui menos itens?
{a: 1} < {a: 1, b: 2}
#=> true

# left contém right e, opcionalmente, possui menos itens?
{a: 1} <= {a: 1}
#=> true

Note que estes novos métodos não interferem na ordenação de hashes, já que o método Hash#<=> permaneceu inalterado.

A proposta original utilizava o método Hash#contain?, mas o Matz achou que este nome era ambíguo (embora ele não tenha especificado, acredito que é por causa do próprio Hash#include?).

Ticket: #10984.

Enumerable#grep_v

O método Enumerable#grep_v é a contra-partida do método Enumerable#grep e emula o comportamento do comando grep -v, ignorando todos os itens que casarem a condição. No exemplo à seguir, estamos ignorando todas as cores que tiverem a letra w.

colors = %w[white black brown]

colors.grep_v(/w/)
#=> ["black"]

Tickets: #11049 e #11773.

Números positivos e negativos

Sugerido pelo Rafael França após serem adicionados ao ActiveSupport, estes métodos permitem verificar se um número é positivo ou não, seguindo a mesma linha dos métodos Numeric#zero? e Numeric#nonzero?.

1.positive?
#=> true

-1.negative?
#=> trues

0.negative?
#=> false

0.positive?
#=> false

Ticket: #11151.

Module.deprecate_constant

O método Module.deprecate_constant permite emitir um aviso quando uma constante é acessada.

module A
  module B
  end
end

A.deprecate_constant(:B)

A::B
#=> warning: constant A::B is deprecated

Um problema desta nova funcionalidade é que não é possível especificar qual é a alternativa para esta constante, caso haja uma. Idealmente deveria ser possível fazer algo como A.deprecate_constant(:B, A::C) para emitir o aviso warning: constant A::B is deprecated; use A::C instead.; se nenhuma alternativa fosse fornecida, exibiria a mensagem do exemplo anterior.

Ticket: #11398.

Did You Mean

O Ruby 2.3 agora possui integração nativa com a gem did_you_mean, que melhora as mensagens de erros para casos de nomes incorretos de classes, métodos e variáveis (typos).

# ruby 2.2
Strin
#=> NameError: uninitialized constant Strin

# ruby 2.3
Strin
#=> NameError: uninitialized constant Strin
#=> Did you mean?  String
#=>                STDIN

O mesmo acontece com nomes de variáveis e métodos.

users = []
#=> []

user
#=> NameError: undefined local variable or method `user' for main:Object
#=> Did you mean?  users

users.revers
#=> NoMethodError: undefined method `revers' for []:Array
#=> Did you mean?  reverse
#=>                reverse!

Ticket: #11252.

Heredoc Strings

O Ruby possui suporte para strings multilinhas. No entanto, em muitos casos é comum usarmos strings heredoc.

sql = <<SQL
SELECT *
FROM users
WHERE user_id = 1
SQL

Perceba que no exemplo anterior, você precisa obrigatoriamente adicionar o delimitador SQL no começo da linha. Este delimitador pode ter o nome que você quiser e, em alguns editores como o Sublime Text, irá ativar o syntax highlighting.

Você também pode usar um outro tipo de heredoc que permite indentar o delimitador. Ele é especialmente útil quando você possui métodos.

class UserFinder
  def self.query
    sql = <<-SQL
      SELECT *
      FROM users
      WHERE user_id = 1
    SQL
  end
end

A maior desvantagem do <<- é que ele irá adicionar os espaçamentos da indentação na string. Isso significa que se você fizer o output daquela string, terá algo como

      SELECT *
      FROM users
      WHERE user_id = 1

em vez de

SELECT *
FROM users
WHERE user_id = 1

Se você tem acesso à biblioteca ActiveSupport, pode usar o método String#strip_heredoc.

class UserFinder
  def self.query
    sql = <<-SQL.strip_heredoc
      SELECT *
      FROM users
      WHERE user_id = 1
    SQL
  end
end

À partir do Ruby 2.3 você não precisa mais do método String#strip_heredoc e agora tem um novo tipo de heredoc. Trata-se de <<~, que ignora todos os espaçamentos da indentação, mantendo as quebras de linha.

class UserFinder
  def self.query
    sql = <<~SQL
      SELECT *
      FROM users
      WHERE user_id = 1
    SQL
  end
end

Ticket: #9098.

Finalizando

O Ruby vem melhorando incrementalmente ao longo dos anos. Depois da migração mais complicada de 1.8 para 1.9, não tivemos tantas mudanças drásticas que dificultaram a migração. E isso é uma característica importante para que a adoção de novas versões seja grande. Além disso, cada versão lançada tem tido melhorias de performance de 5 a 10% em comparação à versão anterior.

Para saber quais são os planos para o Ruby, recomendo que você assista o keynote do Matz apresentado na Rubyconf deste ano. Dentre os objetivos está melhorar o suporte para multi-cores e programação concorrente, tornando a versão 3.0 três vezes mais rápida que a versão 2.0 em uma iniciativa chamada de Ruby3x3. O futuro do Ruby ainda é promissor.


  1. Richard Schneeman conseguiu 12% em um patch do Ruby on Rails onde a estratégia é, basicamente, utilizar strings imutáveis. 

  2. Lembre-se que as opções do interpretador Ruby também podem ser passadas através da variável de ambiente RUBYOPT. Sendo assim, podemos fazer algo como RUBYOPT='--enable-frozen-string-literal' ruby -e 'puts "Ruby".frozen?'