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.
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:
- Qual é a classe de
user? - Essa classe tem
namedefinido? - Se não, qual é a próxima classe na hierarquia?
- Subir até
BasicObject. - 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:
includevsprependmuda 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.
