Метапрограмування в Ruby: attr_accessor

Введення

Метапрограмування — модне віяння у сфері розробки ПЗ, що набирає популярність в сучасних високорівневих мовах програмування. Формально, мета-програмування — це набір практичних прийомів, які дозволяють частково генерувати код програми в run-time за допомогою простішого коду.
 
Основу мета-програмування становить інтроспекція, це — можливість працювати з внутрішньою структурою коду, як з змінними вбудованих типів (переглядати, змінювати, доповнювати визначення типів під час виконання програми, динамічно визначати змінні і працювати з ними і т.д.
 
Ruby — це один з самий яскравих і практичних прикладів реалізації мови, що підтримує цю техніку. Ось кілька прикладів того, як в Ruby підтримується інтроспекція:
 
1) Ruby надає можливість доповнити опис типу, в прикладі нижче, в стандартний клас String ми додали новий метод:
 
 
class String
  def palindrome?()
    self == self.reverse
  end
end

"string sample".polindrome? 
# => false
"godsdog".polindrome?
# => true

 
2) Ruby дозволяє динамічно визначити новий тип і почати працювати з ним негайно:
 
 
NewClass = Class.new(String)
new_class_instance = NewClass.new("hello")
# => "hello"

 
3) Для будь-яких типів Ruby дозволяє динамічно створювати методи, змінні класу і т.д.:
 
 
NewClass = Class.new(String) do
  define_method : polindrome? do
    self == self.reverse
  end
end
nc = NewClass.new("sdfsdf") 
nc. polindrome? 
# => false

 
 

Переходимо до мета-програмування

Класичний для Ruby приклад — створення access-методів для внутрішньої змінної класу:
 
 
class Entity
  attr_accessor  :data
end
e = Entity.new
e.data = “hello”
puts e.data

 
Ніякої магії, attr_accessor — це приватний метод класу Module, який виконується під час визначення класу Entity і створює 2 методу доступу до змінної data : data і data =. Перевіримо дане твердження, виконавши в консолі Ruby наступний код:
 
 
Module.private_methods.include?(:attr_accessor)
# => true, это приватный метод класса Module

class Entity
end

instance_methods_before = Entity.instance_methods

class Entity
  attr_accessor :data
end

Entity.instance_methods - instance_methods_before
# => [:data, :data=] #два новых метода

entity = Entity.new
entity.data  = "hello"
puts entity.data  # @data теперь можно считывать и устанавливать

 
Аналогічним чином можна створити методи на читання і запис окремо, використовуючи для цього різні методи
 
 
class Entity
  attr_reader :data
  attr_writer :data
end

 
Суть процесу: Ruby зустрічає конструкцію class Entity і розуміє, що починається визначення типу Entity, зустрівши конструкцію, наприклад, attr_reader, інтерпретатор викликає відповідний метод, який в свою чергу додає метод у визначення класу.
 
Для розуміння як це працює зсередини, напишемо свою версію методів attr_reader, attr_writer, attr_accessor і назвемо з відповідно my_attr_reader, my_attr_writer, my_attr_accessor. Почнемо з простого. визначимо метод класу my_attr_reader і перевіримо, що виклик цього методу з визначення класу працює:
 
 
class Entity
  def self.my_attr_reader
    puts "my_attr_reader"
  end

  my_attr_reader
end
#my_attr_reader
# => nil 

 
Для прихильників статичних мов програмування виглядає як безумство: як можна викликати метод класу, якщо ще не визначили клас. Але оскільки в Ruby клас визначається динамічно, до моменту виклику attr_reader клас з мінімальним функціоналом вже існує, тому виклик методу цілком коректний.
 
Йдемо далі, очікуємо, що в attr_reader нам передається аргумент і визначаємо метод з таким ім'ям:
 
 
class Entity
  def self.my_attr_reader(var_name)
    define_method(var_name) do
      puts "var_name #{var_name}"
    end
  end

  my_attr_reader :data
end

Entity.instance_methods.include?(:data) #проверяем, есть появился ли такой метод в Entity
 => true 

entity = Entity.new
entity.data #вызываем новый метод
# => "var_name #{data}"

 
Новий метод повинен повернути нам значення змінної data , отримати значення внутрішньої змінної дуже просто за допомогою виклику методу instance_variable_get , єдиний нюанс, що ім'я змінної має починатися з @:
 
 
class Entity
  def self.my_attr_reader(var_name)
    define_method(var_name) do
      instance_variable_get("@#{var_name}")
    end
  end
end

 
Аналогічно пишемо метод my_attr_reader для створення методу-мутаторів, який встановлюватиме значення в локальну змінну:
 
 
class Entity
  def self.my_attr_writer(var_name)
    define_method("#{var_name}=") do |new_value|
      instance_variable_set("@#{var_name}", new_value)
    end
  end
end

 
Збираємо всі разом і дописуємо метод my_attr_accessor на базі двох уже написаних:
 
 
class Entity
  def self.my_attr_reader(var_name)
    define_method(var_name) do
      puts "var_name #{var_name}"
    end
  end

  def self.my_attr_writer(var_name)
    define_method("#{var_name}=") do |new_value|
      instance_variable_set("@#{var_name}", new_value)
    end
  end

  def self.my_attr_accessor(var_name)
    my_attr_writer var_name
    my_attr_reader var_name
  end
end

 
Методи в класі Entity оголошені відкритими, тому ми можемо викликати відповідний код для генерації нових методів по-необхідності в будь-який час:
 
 
Entity.my_attr_accessor :data

 
У загальному випадку, таким методи робляться закритими, щоб використовувати їх виключно в контексті визначення класу. Наведений вище приклад, це лише найпримітивніший приклад метапрограмування в Ruby, але проте використовується повсюдно і наочно показує можливості даного підходу.
 
P.S. У статті використовувався інтерпретатор MRI версії 1.9.3

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.