Metaprogramação em Ruby

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?.

"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.

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.

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.

"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:

"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.

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:

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.

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.

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:

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:

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.

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:

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.

 

Dr. Michael J. Garbade

I, Dr. Michael J. Garbade is the co-founder of the Education Ecosystem (aka LiveEdu), ex-Amazon, GE, Rebate Networks, Y-combinator. Python, Django, and DevOps Engineer. Serial Entrepreneur. Experienced in raising venture funding. I speak English and German as mother tongues. I have a Masters in Business Administration and Physics, and a Ph.D. in Venture Capital Financing. Currently, I am the Project Lead on the community project -Nationalcoronalvirus Hotline I write subject matter expert technical and business articles in leading blogs like Opensource.com, Dzone.com, Cybrary, Businessinsider, Entrepreneur.com, TechinAsia, Coindesk, and Cointelegraph. I am a frequent speaker and panelist at tech and blockchain conferences around the globe. I serve as a start-up mentor at Axel Springer Accelerator, NY Edtech Accelerator, Seedstars, and Learnlaunch Accelerator. I love hackathons and often serve as a technical judge on hackathon panels.

View Comments

Recent Posts

Highest Stable Coin Yields – (W16 – 2024)

Another week to bring you the top yield platforms for three of the most prominent…

2 weeks ago

LEDU Token OTC Trading

If you hold a large volume of LEDU tokens above 1 million units and wish…

1 month ago

Highest Stable Coin Yields – (W12 – 2024)

It’s another week and like always we have to explore the top yield platforms for…

1 month ago

Binance Auto Invest – the Best Innovation in Crypto since Sliced Bread

At a time where we’re constantly seeking tools and strategies to simplify our crypto investments,…

1 month ago

Highest Stable Coin Yields – March 2024

As we kick off another week, it's time to explore the top yield platforms for…

2 months ago

Education Ecosystem Featured on Business Insider

We're excited to share that Education Ecosystem was recently featured in an article on Business…

2 months ago