[Из песочницы] Метапрограммирование в 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