Программирование, которым я периодически по-прежнему занимаюсь, постепенно меняет свой стиль и всё больше связано с метапрограммированием. При этом нельзя сказать, что обычное программирование мне опостылело. Просто как любой программист, я ищу пути для всё большей модульности, краткости, внятности и гибкости кода, и в метапрограммировании мне видится нераскрытый потенциал (несмотря на давний необозримый интернетовский флуд по метапрограммированию идущий ещё от Lisp). :)
Хочу начать вести блог, посвященный метапрограммированию на Ruby.
Выбор Ruby связан с тем, что культура метапрограммирования в среде Ruby-программистов уже во многом сформировалась, и элементы метапрограммирования стали тканью повседневного труда Ruby-программиста, и, кроме того, он мне лучше известен, чем другие динамические языки.
Я читал и читаю лекции по Ruby & Rails & Metaprogramming на Физтехе; материалы одной из лекций можно взять здесь. Там коротко о главном в картинках.
В этом блоге я постараюсь излагать тему последовательно и подробно. Заранее делаю глубокий вдох, потому как поставленная задача не простая. Надеюсь на ваш подбадривающий фидбэк.
Начну с простого — с определения.
Метапрограммирование в скриптовых языках — это стиль написания программ, при котором с пользой используются возможности изменения пространства имен в runtime.
Под пространством имён имеется в виду классы, методы и переменные (глобальные, локальные, переменный экземпляра и переменные класса). Изменение означает создание, изменение и удаление классов, методов и переменных.
Надо сказать, что в большинстве скриптовых языков пространство имен конструируется не иначе как в режиме runtime. Но многие об этом не помнят, поэтому я это подчеркнул в определении. Если убрать из определения лишнее упоминание про runtime, то останется словосочетание «с пользой». Значит в нём и суть.
Пример безполезного неметапрограммирования:
Помню как в глубоком детстве я писал для БК-0010 программу построения графиков функций. Функции хардкодились и при работе программы можно было лишь выбрать одну функцию из списка и указать диапазон [x0, x1], а диапазон по оси Y (о чудо программистской мысли! способной автоматизировать всё и вся) выбирался программой автоматически.
Смотрел я на свою программу на Бейсике и переживал экстаз. Но тут меня посетила грустная мысль: «Эх!!! А жаль все таки, что нельзя прямо во время выполнения программы вбить формулу нужной мне функции.»
Ндаа… 8-й класс, 1992 год, г. Кирово-Чепецк. Много с той поры воды утекло, а проблемы всё те же!
К чему это я?
Вот вам код интерактивного «калькулятора» на языке Ruby:
или получше
или правильно
require 'readline'
while line = Readline.readline("> ")
begin
puts eval(line).inspect
rescue => e
puts e.to_s + "\n" + e.backtrace.join("\n")
end
end
Пример выполнения:
В скриптовых языках есть метод
Собственно, средства, подобные
Кстати, метод
Для определения атрибутов экземпляров класса конструкцией
Смысл выражения
class Song
attr_accessor :title, :length
end
равносилен (по результату) коду
class Song
def title
@title
end
def title=(v)
@title = v
end
def length
@length
end
def length=(v)
@length = v
end
end
Вот вам и определение!
Невооруженным взглядом видно, что
Конструкция
def attr_accessor(*methods)
methods.each do |method|
eval %{
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
}
end
end
Теперь
Возможность писать методы, подобные
Определение функции выполнилось? Перешли в контекст класса? Что за бред? — спросит неизвестно откуда взявшийся здесь программист С++. Да, именно так.
"
Текст "
A: Это такой контекст, в котором выражение self равно некоторому классу.
Выполните следующий код:
puts "hi1 from #{self.inspect}"
class Abc
puts "hi2 from #{self.inspect}"
def hi
puts "hi3 from #{self.inspect}"
end
end
Будут напечатаны строки с hi1 и hi2. Строку с hi3 вы увидите, если допишете
Итого получите:
Надо понимать, что когда вы пишете
то по сути спереди неявно подставляется "
Ладно, хватит разглагольствовать. Исправим указанный выше код
class Module
def attr_accessor(*methods)
methods.each do |method|
class_eval %{
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
}
end
end
end
Что мы сделали? Мы поместили определение метода в контекст класса
Почему мы так сделали? Есть причины:
* Нехорошо писать методы без понимания того, для каких объектов они будут доступны. Нам нужно написать метод
* Метод
Итак, теперь код работает. Но он неправильный. Есть и другие неправильные решения, в том числе без "
def attr_accessor(*methods)
methods.each do |method|
eval %{
class #{self}
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
end
}
end
end
Последний вариант плох тем, что его можно вызвать не в контексте класса и получить что-то нехорошее, зависящее от того, чему в этом контексте равно выражение
s = "Class"
s.instance_eval { attr_accessor :hahaha}
Array.hahaha = 3 # неожиданным образом у Array появился атрибут hahaha
puts Array.hahaha #
САМОЕ ВАЖНОЕ:
Описанные определения
Напишем, наконец то, правильный вариант определения
class Module
def attr_accessor(*methods)
methods.each do |method|
raise TypeError.new("method name is not symbol") unless method.is_a?(Symbol)
define_method(method) do
instance_variable_get("@#{method}")
end
define_method("#{method}=") do |v|
instance_variable_set("@#{method}", v)
end
end
end
end
Мы часто пишем атрибуты с дефолтным значением. Делаем мы это, используя идиому "
после такого определения, значение атрибута
По щучьему веленью, по моему хотенью,… пусть данный код работает так, как я хочу!!!:
Напишем исключительно в учебных целях неправильный код, использующий
class Module
def attr_accessor(*methods)
options = methods.last.is_a?(Hash)? methods.pop: {}
methods.each do |method|
class_eval %{
def #{method}
\# не пишите так никогда!
@#{method} #{ "||= #{options[:default]}" if options[:default] }
end
def #{method}=(v)
@#{method} = v
end
}
end
end
end
Да будет чудо!!!
Неправильный код тоже иногда работает. Но это, конечно, не повод не быть уволеным тому программисту, который его напишет.
При выполнении
class Song
attr_accessor :length, :default => 42
attr_accessor :title, :default => "no title"
end
puts Song.new.length # выводит 42!!!
puts Song.new.title # oooooops!!!
получаем загадочное:
Почему возникла такая неприятность? Дело в том, что есть фундаментальная проблема: вставки внутрь строки некоторых объектов просто невозможны.
Правильно задача об
class Module
def attr_accessor(*methods)
options = methods.last.is_a?(Hash)? methods.pop: {}
methods.each do |method|
raise TypeError.new("method name is not symbol") unless method.is_a?(Symbol)
define_method(method) do
instance_variable_get("@#{method}") ||
instance_variable_set("@#{method}", options[:default])
end
define_method("#{method}=") do |v|
instance_variable_set("@#{method}", v)
end
end
end
end
Итак, в рассмотренных примерах метапрограммирование выглядит как написание методов определяющих методы.
Начинающему метапрограммисту имеет смысл погуглить такие поисковые запросы:
1. ruby doc attr_accessor
2. ruby doc Kernel eval
3. ruby doc Module class_eval
4. ruby doc Object instance_eval
5. ruby doc Object is_a?
Первые ссылки верны.
Как метапрограммировать без
Хочу начать вести блог, посвященный метапрограммированию на Ruby.
Выбор Ruby связан с тем, что культура метапрограммирования в среде Ruby-программистов уже во многом сформировалась, и элементы метапрограммирования стали тканью повседневного труда Ruby-программиста, и, кроме того, он мне лучше известен, чем другие динамические языки.
Я читал и читаю лекции по Ruby & Rails & Metaprogramming на Физтехе; материалы одной из лекций можно взять здесь. Там коротко о главном в картинках.
В этом блоге я постараюсь излагать тему последовательно и подробно. Заранее делаю глубокий вдох, потому как поставленная задача не простая. Надеюсь на ваш подбадривающий фидбэк.
Начну с простого — с определения.
Метапрограммирование в скриптовых языках — это стиль написания программ, при котором с пользой используются возможности изменения пространства имен в runtime.
Под пространством имён имеется в виду классы, методы и переменные (глобальные, локальные, переменный экземпляра и переменные класса). Изменение означает создание, изменение и удаление классов, методов и переменных.
Надо сказать, что в большинстве скриптовых языков пространство имен конструируется не иначе как в режиме runtime. Но многие об этом не помнят, поэтому я это подчеркнул в определении. Если убрать из определения лишнее упоминание про runtime, то останется словосочетание «с пользой». Значит в нём и суть.
Пример безполезного неметапрограммирования:
eval "s = 'eval s'; eval s"
Калькулятор
Помню как в глубоком детстве я писал для БК-0010 программу построения графиков функций. Функции хардкодились и при работе программы можно было лишь выбрать одну функцию из списка и указать диапазон [x0, x1], а диапазон по оси Y (о чудо программистской мысли! способной автоматизировать всё и вся) выбирался программой автоматически.
Смотрел я на свою программу на Бейсике и переживал экстаз. Но тут меня посетила грустная мысль: «Эх!!! А жаль все таки, что нельзя прямо во время выполнения программы вбить формулу нужной мне функции.»
Ндаа… 8-й класс, 1992 год, г. Кирово-Чепецк. Много с той поры воды утекло, а проблемы всё те же!
К чему это я?
Вот вам код интерактивного «калькулятора» на языке Ruby:
while line = readline puts eval(line).inspect end
или получше
while (print "> "; true) and line = readline puts eval(line).inspect end
или правильно
require 'readline'
while line = Readline.readline("> ")
begin
puts eval(line).inspect
rescue => e
puts e.to_s + "\n" + e.backtrace.join("\n")
end
end
Пример выполнения:
artem@laptop:~/meta-lectures$ ruby console.rb > 1+2 3 > "hello" "hello" > def fib(n) (0..1)===n ? 1 : fib(n-1)+fib(n-2) end nil > (0...10).map{|n| fib(n)} [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] > 1/0 (eval):1:in `/': divided by 0 console.rb:4 (eval):1 > exit artem@laptop:~/meta-lectures$
В скриптовых языках есть метод
eval
, который получает строку и выполняет эту строку в текущем контексте так (почти так), как если бы она была написана программистом в месте вызова eval
.Собственно, средства, подобные
eval
, есть и в компилируемых языках программирования.Кстати, метод
eval
я бы не относил к метапрограммированию и даже назвал бы его крайне вредным методом для этого занятия. Интерактивный ruby|python|perl|...-shell — пожалуй, один из немногих примеров, где его стоит применять. О вреде метода eval
поговорим далее.attr_accessor
Для определения атрибутов экземпляров класса конструкцией
attr_accessor
пользуются даже рубисты новички, не всегда правда понимая, что это за зверь.Смысл выражения
attr_accessor
выведите из следующего утверждения: код class Song
attr_accessor :title, :length
end
равносилен (по результату) коду
class Song
def title
@title
end
def title=(v)
@title = v
end
def length
@length
end
def length=(v)
@length = v
end
end
Вот вам и определение!
Невооруженным взглядом видно, что
attr_accessor
полезен, так как отвечает внутреннему стремлению программиста сделать код кратким и внятным. Конструкцию attr_accessor
можно переводить как «Хочу set- и get- методы для следующих атрибутов экземпляров класса».Конструкция
attr_accessor
вовсе даже не неведомый зверь (читай — не есть встроенная конструкция языка), а обычный метод, который можно запрограммировать самим. Давайте это сделаем с помощью метода eval
.def attr_accessor(*methods)
methods.each do |method|
eval %{
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
}
end
end
Теперь
attr_accessor
, получая в качестве аргумента массив названий атрибутов, для каждого названия выполняет строчки кода, определяющие соответствующие атрибуту set- и get- методы.Возможность писать методы, подобные
attr_accessor
появилась потому, что в Ruby нет понятия определения класса или метода. Написав строку "class Song
" мы просто перешли в некоторый новый контекст, в котором можно заниматься обычными вычислениями, и конструкция "def xxx() ... end
" лишь одно из выражений, результат вычисления которого всегда равен nil
(в ruby v1.8), а сторонний эффект проявляется в появлении метода "xxx
" у класса, в контексте которого эта конструкция выполнилась.Определение функции выполнилось? Перешли в контекст класса? Что за бред? — спросит неизвестно откуда взявшийся здесь программист С++. Да, именно так.
"
class Song
" не обрамляет спереди определение класса, а осуществляет переход в специальный контекст, в котором меняется область видимости пространства имен; то есть появляются некоторые новые методы, которые мы можем вызвать в данном контексте, меняются значения и эффект от выполнения некоторых инструкций и т.д. и т.п.Текст "
def xxx() ... end
" действительно является выражением и выполняется виртуальной машиной Ruby. При этом внутренность определения метода не выполняется, а транслируется в байт код и запоминается под именем метода. Q: Что значит контекст класса?
A: Это такой контекст, в котором выражение self равно некоторому классу.
Выполните следующий код:
puts "hi1 from #{self.inspect}"
class Abc
puts "hi2 from #{self.inspect}"
def hi
puts "hi3 from #{self.inspect}"
end
end
Будут напечатаны строки с hi1 и hi2. Строку с hi3 вы увидите, если допишете
Abc.new.hi
Итого получите:
artem@laptop:~/meta-lectures$ ruby self_in_contexts.rb hi1 from main hi2 from Abc hi3 from #<Abc:0xb7c3d9dc> artem@laptop:~/meta-lectures$
Надо понимать, что когда вы пишете
my_method(arg1,arg2)
то по сути спереди неявно подставляется "
self.
":self.my_method(arg1,arg2)
Но эти два выражения не эквивалентны в некоторых случаях.
Например, когда
my_method
является private
-методом, то выражение self.my_method
даст ошибку вызова private
метода. Это особенности реализации Ruby — private
-методы и есть такие методы, которые нельзя вызывать через точку.Ладно, хватит разглагольствовать. Исправим указанный выше код
attr_accessor
, чтобы он стал работающим:class Module
def attr_accessor(*methods)
methods.each do |method|
class_eval %{
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
}
end
end
end
Что мы сделали? Мы поместили определение метода в контекст класса
Module
и заменили eval
на class_eval
. Почему мы так сделали? Есть причины:
* Нехорошо писать методы без понимания того, для каких объектов они будут доступны. Нам нужно написать метод
attr_accessor
, который можно использовать в контексте классов (экземпляров класса Class
) и модулей (экземпляров класса Module
). Класс Class
наследует от класса Module
, поэтому достаточно определить этот метод как метод экземпляров Module
, тогда он будет доступен как для модулей так и для классов.* Метод
class_eval
имеет свои отличия от eval
, в частности последний при выполнении выражения "def ... end
" будет создавать определение метода локально живущего внутри метода attr_accessor
и доступного только во время выполнения метода attr_accessor
(это незадокументированная фича "def
внутри def
"). Метод class_eval
выполняет заданный код в правильном контексте, так что "def
" начинают приводить к нужному результату. Метод class_eval
активно и используется в метапрограммировании именно в варианте, когда его аргументом является блок, а не строка.Итак, теперь код работает. Но он неправильный. Есть и другие неправильные решения, в том числе без "
class Module
" и "class_eval
". Вот одно из них:def attr_accessor(*methods)
methods.each do |method|
eval %{
class #{self}
def #{method}
@#{method}
end
def #{method}=(v)
@#{method} = v
end
end
}
end
end
Последний вариант плох тем, что его можно вызвать не в контексте класса и получить что-то нехорошее, зависящее от того, чему в этом контексте равно выражение
self
. Например:s = "Class"
s.instance_eval { attr_accessor :hahaha}
Array.hahaha = 3 # неожиданным образом у Array появился атрибут hahaha
puts Array.hahaha #
САМОЕ ВАЖНОЕ:
Описанные определения
attr_assessor
с использованием eval
плохи своей несекьюрностью — они не защищены ни от злого умысла врага, ни от глупости самого программиста: если значение переменной method
не является валидной строкой для имени метода, а например, равно строке "llalala(); puts `cat /etc/passwd`; puts
", то последствия будут непредсказуемые. Никаких ошибок (исключений) при выполнении программы вы можете и не увидеть; сюрпризы полезут лишь тогда, «когда ракета будет уже лететь» (с). Нет же ничего хуже ошибок, которые проявляются с запозданием, когда концов уже не сыщешь.Напишем, наконец то, правильный вариант определения
attr_accessor
. Он, в отличие от неправильных, единственен:class Module
def attr_accessor(*methods)
methods.each do |method|
raise TypeError.new("method name is not symbol") unless method.is_a?(Symbol)
define_method(method) do
instance_variable_get("@#{method}")
end
define_method("#{method}=") do |v|
instance_variable_set("@#{method}", v)
end
end
end
end
attr_accessor
с дефолтным значением
Мы часто пишем атрибуты с дефолтным значением. Делаем мы это, используя идиому "
||=
", которая грубо переводится как «инициализировать то, что слева, тем, что справа, если оно ещё не инициализировано»:class Song
def length
@length ||= 0
end
def title
@title ||= "no title"
end
end
Song.new.length #=> 0
Song.new.title #=> "no title"
после такого определения, значение атрибута
length
новой песни будет равно 0.По щучьему веленью, по моему хотенью,… пусть данный код работает так, как я хочу!!!:
class Song attr_accessor :length, :default => 0 attr_accessor :title, :default => "no title" end
Напишем исключительно в учебных целях неправильный код, использующий
class_eval
от строки:class Module
def attr_accessor(*methods)
options = methods.last.is_a?(Hash)? methods.pop: {}
methods.each do |method|
class_eval %{
def #{method}
\# не пишите так никогда!
@#{method} #{ "||= #{options[:default]}" if options[:default] }
end
def #{method}=(v)
@#{method} = v
end
}
end
end
end
Да будет чудо!!!
class Song attr_accessor :length, :default => 42 end puts Song.new.length # выводит 42!!!
Неправильный код тоже иногда работает. Но это, конечно, не повод не быть уволеным тому программисту, который его напишет.
При выполнении
class Song
attr_accessor :length, :default => 42
attr_accessor :title, :default => "no title"
end
puts Song.new.length # выводит 42!!!
puts Song.new.title # oooooops!!!
получаем загадочное:
artem@laptop:~/meta-lectures$ ruby bad_attr_accessor.rb 42 (eval):5:in `title': stack level too deep (SystemStackError) from (eval):5:in `title' from bad_attr_accessor.rb:27 artem@laptop:~/meta-lectures$
Почему возникла такая неприятность? Дело в том, что есть фундаментальная проблема: вставки внутрь строки некоторых объектов просто невозможны.
Правильно задача об
attr_accessor
с дефолтным значением решается так:class Module
def attr_accessor(*methods)
options = methods.last.is_a?(Hash)? methods.pop: {}
methods.each do |method|
raise TypeError.new("method name is not symbol") unless method.is_a?(Symbol)
define_method(method) do
instance_variable_get("@#{method}") ||
instance_variable_set("@#{method}", options[:default])
end
define_method("#{method}=") do |v|
instance_variable_set("@#{method}", v)
end
end
end
end
Итак, в рассмотренных примерах метапрограммирование выглядит как написание методов определяющих методы.
Начинающему метапрограммисту имеет смысл погуглить такие поисковые запросы:
1. ruby doc attr_accessor
2. ruby doc Kernel eval
3. ruby doc Module class_eval
4. ruby doc Object instance_eval
5. ruby doc Object is_a?
Первые ссылки верны.
Как метапрограммировать без
eval
, а также о примесях, о методах модификаторах, позволяющих перевести на новый уровень абстракции задачи, связанные с кешированием, RPC, DSL, о паттернах, продолжающих идеи отложенных (ленивых) вычислений и др. читайте в следующих выпусках блога.