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-literal
2.
$ 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
.
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
- Toda vez que um objeto precisa ser convertido em
Proc
, o métodoto_proc
é executado. Seria o equivalente a converter o objeto em bloco. Isso é feito implicitamente quando usamosarray.map(&hash)
, por exemplo. - 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 emhash.to_proc.call(:a)
. - 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"]
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.
Richard Schneeman conseguiu 12% em um patch do Ruby on Rails onde a estratégia é, basicamente, utilizar strings imutáveis. ↩
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 comoRUBYOPT='--enable-frozen-string-literal' ruby -e 'puts "Ruby".frozen?'
. ↩