Neste artigo, vamos ver aspectos diferentes da metaprogramação em Ruby, mas, antes de começar, você precisa saber o que é metaprogramação.
Metaprogramação é a programação de programas que escrevem ou manipulam outros programas (ou a si próprios) assim como seus dados, ou que fazem parte do trabalho em tempo de compilação. Isso permite que os programadores sejam mais produtivos ao evitar que parte do código seja escrita manualmente.
Ruby é uma linguagem de programação interpretada multiparadigma, de tipagem dinâmica e forte, com gerenciamento de memória automático, originalmente planejada e desenvolvida no Japão em 1995, por Yukihiro “Matz” Matsumoto, para ser usada como linguagem de script.
O livecoder brasileiro LucasMRThomaz desenvolveu no LiveEdu um blog com Ruby on Rails. Assista abaixo ao seu último vídeo:
Metaprogramação em Ruby é, na realidade, bem simples e isso se dá pelo fato de que todo código Ruby é executado, não há separação entre fases de compilação e runtime, cada linha de código é executado contra um self particular.
Vamos analisar especificamente como podemos ler e analisar o nosso código em Ruby, como podemos chamar métodos (ou enviar mensagens) dinamicamente e como podemos gerar novos métodos durante o tempo de execução do nosso programa.
Fazendo perguntas ao nosso código
Um aspecto da metaprogramação em Ruby que se destaca é ser capaz de perguntar ao nosso código questões sobre si mesmo durante o tempo de execução. Isso também é conhecido como introspecção. Assim como podemos nos fazer perguntas como “Por que estou aqui?”, nosso código pode fazer o mesmo, embora as perguntas não possam ser tão existenciais.
Sou capaz de responder a esta chamada de método?
Podemos perguntar a qualquer objeto se ele tem a capacidade de fornecer uma resposta a uma chamada de método específico antes de fazê-lo usando o método respond_to?.
1 2 3 4 5 6 7 |
"Roberto Alomar".respond_to? :downcase # => true "Roberto Alomar".respond_to? :floor # => false |
Qual é a aparência da minha cadeia de ancestralidade?
Se você verificar um modelo ActiveRecord no Rails 5, verá que ele tem 71 antepassados. Isso inclui os pais diretos através da hierarquia de classes e também os módulos que estão incluídos em qualquer árvore de classe. Isso é um pouco louco e vai mostrar o quão grande é o Rails.
1 2 3 4 5 6 7 |
School.ancestors.size # => 71 String.ancestors # => [String, Comparable, Object, Kernel, BasicObject] |
Quais variáveis de instância e métodos foram definidos?
Podemos usar o método methods para nos fornecer uma lista de todos os métodos disponíveis para um objeto específico e o método instance_variables para nos fornecer uma lista das variáveis de instância definidas/usadas por este objeto.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
require 'date' class Alpaca attr_accessor :name, :birthdate def initialize(name, birthdate) @name = name @birthdate = birthdate end def spit "Putsuuey" end end spitty = Alpaca.new('Spitty', Date.new(1990, 10, 10)) spitty.methods # => [:name, :name=, :birthdate, :spit, :birthdate=, :instance_of?, :public_send, :instance_variable_get, :instance_variable_set, :instance_variable_defined?, :remove_instance_variable, :private_methods, :kind_of?, :instance_variables, :tap, :is_a?, :extend, :define_singleton_method, :to_enum, :enum_for, :<=>, :===, :=~, :!~, :eql?, :respond_to?, :freeze, :inspect, :display, :send, :object_id, :to_s, :method, :public_method, :singleton_method, :nil?, :hash, :class, :singleton_class, :clone, :dup, :itself, :taint, :tainted?, :untaint, :untrust, :trust, :untrusted?, :methods, :protected_methods, :frozen?, :public_methods, :singleton_methods, :!, :==, :!=, :__send__, :equal?, :instance_eval, :instance_exec, :__id__] spitty.instance_variables # => [:@name, :@birthdate] |
Enviando mensagens
Ruby é uma linguagem dinâmica. Consiste de uma série de objetos que podem passar mensagens de um lado para outro entre si. Esta passagem de mensagem é geralmente o que nos referimos quando dizemos “chamar um método”. Vamos dar uma olhada no método downcase de objetos String.
1 2 3 |
"Roberto Alomar".downcase # => "roberto alomar" |
Quando invocamos ou chamamos esse método usando a notação de ponto, o que estamos realmente dizendo é que estamos passando uma mensagem para a String, e ela decide como responder a essa mensagem. Neste caso, ele responde com uma versão minúscula de si mesma.
Vamos entender melhor. Há três partes com as quais estamos trabalhando: a primeira, “Roberto Alomar“, é o objeto, aquele que receberá esta mensagem. O . (ponto) diz ao objeto receptor que estaremos enviando algum comando ou mensagem. O que se segue após o ponto, downcase, é a mensagem que estamos enviando. Podemos dizer que estamos enviando a mensagem downcase para “Roberto Alomar“. Ele descobre o que fazer ou envia de volta uma vez que recebe essa mensagem.
Em Ruby, isso pode ser feito de outra maneira, usando o método send:
1 2 3 |
"Roberto Alomar".send(:downcase) # => "roberto alomar" |
Geralmente, você não usaria esse formulário na programação normal, mas como o Ruby nos permite enviar mensagens (ou invocar métodos) neste formulário, ele dá a opção de enviar uma mensagem dinâmica ou chamar métodos dinamicamente.
1 2 3 4 5 |
method = :downcase "Roberto Alomar".send(method) # => "roberto alomar" |
Isso pode não parecer muita coisa, mas esta é uma das construções que nos permite escrever código muito dinâmico em Ruby. Na próxima seção, veremos como podemos gerar novo código dinamicamente em Ruby usando o método define_method.
Gerando novos métodos
Outro aspecto da metaprogramação que Ruby nos dá é a capacidade de gerar novo código durante o tempo de execução. Faremos isso usando um método da classe Module chamado define_method. Este método funciona passando um símbolo que se torna o nome do nosso novo método e, ao fornecer um bloco, damos ao nosso novo método seu corpo. Aqui está um exemplo simples:
1 2 3 4 5 6 7 8 9 |
class Person define_method :greeting, -> { puts 'Hello!' } end Person.new.greeting # => Hello! |
Você já deve ter visto o método delegate antes, que vem no ActiveSupport com Rails e estende Módulo. Isso nos permite dizer que quando você chama um determinado método, você chama esse método em um objeto diferente ao invés do atual (self). Nós vamos criar uma versão muito mais simples deles como uma maneira de mostrar a metaprogramação. Você pode ver o código fonte para a versão Rails aqui.
Primeiro, vamos adicionar um novo método à classe Module (que todas as classes têm em sua cadeia de ancestrais) chamado delegar.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Module def delegar(method, to:) define_method(method) do |*args, &block| send(to).send(method, *args, &block) end end end |
Quando esse método é chamado, ele irá definir um novo método cujo trabalho é delegar o trabalho para outro objeto, como um proxy.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
class Receptionist def phone(name) puts "Hello #{name}, I've answered your call." end end class Company attr_reader :receptionist delegar :phone, to: :receptionist def initialize @receptionist = Receptionist.new end end company = Company.new company.phone 'Leigh' # => "Hello Leigh, I've answered your call." |
Você pode ver que nós chamamos o método do telefone na Company, mas é o Receptionist que responde realmente a chamada.
Dólares e centavos
Você provavelmente já ouviu falar que é ruim armazenar e usar o dinheiro como um float por causa de questões de aritmética de ponto flutuante. Uma das maneiras de lidar com isso é armazenar o dinheiro em centavos. $10.25 seria armazenado no banco de dados como 1025 centavos.
Entretanto, os usuários não vão querer armazenar valores em centavos, então precisamos de algum código para nos ajudar a converter doláres e centavos. Vamos usar a metaprogramação para nos ajudar a tornar as coisas mais fáceis.
Vejamos uma classe chamada Purchase que tem um campo no banco de dados chamado price_cents. Esta é a aparência da classe:
1 2 3 4 5 6 7 8 9 |
class Purchase attr_accessor :price_cents extend MoneyFields money_fields :price end |
Se este fosse um objeto ActiveRecord no Rails, não teríamos que incluir a linha attr_accessor: price_cents porque já faria isso por nós, mas para este exemplo, estamos apenas usando um objeto Ruby antigo. Este código agora nos dá a capacidade de interagir com o campo da seguinte forma:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
purchase = Purchase.new purchase.price = 10.25 purchase.price_cents # => 1025 purchase.price_cents = 555 purchase.price # => #<BigDecimal:7fbc7497ac88,'0.555E1',18(36)> |
Mas de onde vieram os métodos price e price= ? Nosso método money_fieldsmethod acaba criando estes dois novos métodos que interagem com os métodos price_cents e price_cents= que vêm da linha attr_accessor ou existem para nós a partir do ActiveRecord.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
module MoneyFields require 'bigdecimal' def money_fields(*fields) fields.each do |field| define_method field do value_cents = send("#{field}_cents") value_cents.nil? ? nil : BigDecimal.new(value_cents / BigDecimal.new("100")) end define_method "#{field}=" do |value| value_cents = value.nil? ? nil : Integer(BigDecimal.new(String(value)) * 100) send("#{field}_cents=", value_cents) end end end end |
O método money_fields passa através de um ou mais campos que foram passados ao método criando métodos de leitor e escritor para a forma de dólar do campo. Para mostrar que ele funciona como esperado, aqui está um conjunto de testes que testa as diferentes conversões:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
require 'minitest/autorun' class PurchaseTest < MiniTest::Test attr_reader :purchase def setup @purchase = Purchase.new end def test_reading_writing_dollars purchase.price = 5.00 assert_equal purchase.price, 5.00 end def test_converting_to_dollars purchase.price_cents = 500 assert_equal purchase.price, 5.00 end def test_converting_to_cents purchase.price = 5.00 assert_equal purchase.price_cents, 500 end def test_writing_dollars_from_string purchase.price = "5.00" assert_equal purchase.price_cents, 500 end def test_nils purchase.price = nil assert_equal purchase.price, nil end def test_creating_methods assert_equal Purchase.instance_methods(false).sort, [:price_cents, :price_cents=, :price, :price=].sort end def test_respond_to_dollars assert_equal purchase.respond_to?(:price), true assert_equal purchase.respond_to?(:price=), true end end |
Conclusão
Metaprogramação é fantástico, mas apenas quando é usado com moderação. A metaprogramação pode ajudá-lo a escrever código repetitivo mais facilmente (como o exemplo de campos de dinheiro), pode ajudá-lo a depurar e analisar o que seu código está fazendo, mas também pode adicionar indireção e torná-lo muito mais difícil de descobrir o que está realmente acontecendo no código. Use somente a metaprogramação se ela fornecer uma clara vantagem.
A maioria dos métodos que vimos hoje vem da classe Object ou da classe Module. Explore mais por sua conta!
No ano passado, a CBSI divulgou uma ótima apostila gratuita de 170 páginas sobre Ruby para você conhecer mais desta linguagem que a cada dia ganha mais usuários.
Praticamente, toda a comunidade brasileira já deve ter ouvido falar de Fábio Akita, co-fundador e CTO da Codeminer 42 e co-organizador da Rubyconf Brasil. Se você quiser conhecer melhor este evangelizador do Ruby, confira a entrevista que o Bugginho Developer fez com ele.