githublinkedinemail
← /articles

Como Ruby resolve métodos internamente

Quando você chama user.name, Ruby faz um ritual. Method lookup, inline cache, method_missing, singleton classes — a mecânica real por trás de uma chamada simples.

4 minruby

Como Ruby resolve métodos internamente

Você escreve:

user.name

E pensa: "Ruby chamou o método name no objeto user."

Errado.

Ruby fez um ritual.

E entender esse ritual é a diferença entre saber Ruby e saber Ruby.


O que acontece quando você chama um método

Quando você invoca user.name, Ruby precisa responder:

  1. Qual é a classe de user?
  2. Essa classe tem name definido?
  3. Se não, qual é a próxima classe na hierarquia?
  4. Subir até BasicObject.
  5. Não achou? NoMethodError.

Esse passo a passo se chama method lookup.

E é onde Ruby gasta tempo silenciosamente.


A classe não é onde você acha que está

Quando você faz:

user = User.new

Você acha que user.class == User?

Em parte sim.

Mas tem mais coisa. Cada objeto carrega uma classe singleton (também chamada eigenclass ou metaclass).

user
 ↓
[singleton class de user]
 ↓
User
 ↓
ApplicationRecord
 ↓
ActiveRecord::Base
 ↓
Object
 ↓
Kernel (módulo incluído)
 ↓
BasicObject

Esse é o caminho que Ruby percorre toda vez que você chama um método.


Method lookup na prática

class A
  def hello = "A#hello"
end

class B < A
  def hello = "B#hello"
end

B.new.hello # => "B#hello"

Por quê?

Porque Ruby olhou B primeiro. Achou hello lá. Parou.

Nem chegou em A.

Agora:

class A
  def hello = "A#hello"
end

class B < A
end

B.new.hello # => "A#hello"

Ruby olhou B, não achou. Subiu pra A. Achou. Usou.

Esse "subir" é literal. Tem uma chain. Ruby caminha por ela.


Módulos entram no meio da chain

module Greetable
  def hello = "from module"
end

class User
  include Greetable
end

User.ancestors
# => [User, Greetable, Object, Kernel, BasicObject]

include insere o módulo acima da classe e abaixo da próxima ancestral.

prepend insere abaixo da própria classe — entre user e User. Atalho cirúrgico pra interceptar métodos.

module Logger
  def hello
    puts "[log] chamando hello"
    super
  end
end

class User
  prepend Logger
  def hello = "hello"
end

User.new.hello
# [log] chamando hello
# => "hello"

Isso é como Active Support patcheia coisas em Rails sem você notar.


Inline cache: o salvador silencioso

Se Ruby fizesse esse lookup completo toda vez que você chama um método, performance ia ser horrível.

Por isso a VM tem um inline method cache (IC).

A cada call site, Ruby lembra:

  • qual era a classe do receiver
  • qual método foi encontrado

Próxima vez que o mesmo call site é executado com a mesma classe, Ruby pula o lookup. Vai direto.

1000.times { user.name }  # IC hit em 999 chamadas

Hit no cache = rápido.

Cache miss = lookup completo.


O que invalida o cache

  • definir método novo em runtime (define_method, monkey patch)
  • mudar a classe de um objeto (extend, singleton_method)
  • redefinir constantes
  • alterar hierarquia (raro, mas existe)

Quando você faz metaprogramação pesada em runtime, está continuamente invalidando caches.

Por isso meta-programação tem custo.

Por isso method_missing é lindo mas caro.


method_missing e respond_to_missing?

Quando o lookup falha completamente (chegou em BasicObject sem achar), Ruby chama method_missing no receiver.

class GhostMethod
  def method_missing(name, *args)
    "vc chamou #{name}"
  end
end

GhostMethod.new.qualquer_coisa
# => "vc chamou qualquer_coisa"

Útil. Perigoso.

Cada chamada via method_missing é o lookup inteiro falhando. Sem inline cache. Sempre lento.

E pior: respond_to? mente se você não implementar respond_to_missing? também.


Singleton methods quebram a regra

user = User.new
def user.special_method
  "só eu tenho isso"
end

user.special_method # => "só eu tenho isso"
User.new.special_method # NoMethodError

Onde esse método mora?

Na singleton class daquele objeto específico.

user
 ↓
[singleton de user] — special_method está aqui
 ↓
User
 ↓
...

Toda vez que você faz class << self, está abrindo essa classe.

Métodos de classe (def self.foo) na verdade são singleton methods da classe (que também é um objeto).

Tudo em Ruby é objeto.

Inclusive classes.


Por que isso importa em produção

  • Performance: muita meta-programação dinâmica = cache misses constantes = mais CPU
  • Debugging: entender por que um método foi sobrescrito requer entender a chain
  • Bugs sutis: include vs prepend muda quem ganha
  • Frameworks: Rails, Devise, AR concerns — tudo joga com a ancestors chain

Você não precisa pensar nisso todo dia.

Mas no dia que precisar, ou você sabe, ou está perdido.


A grande virada de chave

Ruby não "chama um método".

Ruby resolve um método.

E resolução é processo. Tem ordem. Tem custo. Tem cache. Tem invalidação.

Quando você entende isso, metaprogramação para de ser místico.

E debug de Rails (que é metaprogramação levada ao extremo) começa a fazer sentido.


Conclusão

Method lookup é uma das coisas mais lindas e perigosas do Ruby.

Lindo porque permite a expressividade absurda da linguagem.

Perigoso porque você pode acidentalmente:

  • sobrescrever método sem perceber
  • destruir performance com method_missing
  • criar bugs invisíveis com prepend

Ruby te dá liberdade demais.

E quem entende o ritual de lookup é quem usa essa liberdade bem.


Como Ruby resolve métodos internamente
Como Ruby resolve métodos internamente