Кодогенерация
Мы видим, что с увеличением сложности структуры данных усложняется и реализация ключевых слов, и сопутствующего им кода. Растут накладные расходы по производительности загрузки данных. В последнем примере требуется, как минимум, пять вложенных вызовов функций, прежде чем дело доходит до непосредственного создания объекта. В действительности это не очень страшно – виртуальная машина Lua обладает очень высокой производительностью и, к тому же, поддерживает «хвостовые» вызовы функций (tail function calls) (и, в частности, хвостовую рекурсию, tail recursion), что позволяет эффективно оптимизировать вызовы вида return functioncall и снимает ограничение на количество вложенных вызовов функций такого рода.
Тем не менее, может возникнуть необходимость оптимизации процесса загрузки данных и создания из них «живых» объектов логики приложения. Наиболее эффективный способ сделать это – убрать всякую загрузку данных и оставить только код создания и настройки объектов с зашитыми намертво параметрами, узкоспециализированный под конкретный рассматриваемый случай и набор данных; уничтожить всю архитектурную прослойку в коде и оставить только нужную в данный момент функциональность.
Существует способ добиться такого эффекта автоматически и относительно бесплатно на основе имеющихся данных в описываемом формате. Впрочем, такой способ эффективен только в том случае, когда данные статичны относительно основного приложения, т.е. число загрузок данных значительно превышает число их изменений. Рассматриваемый в статье пример с пользовательским интерфейсом игры – как раз такой случай. При его создании требуется гибкость и легкость внесения изменений, но эти свойства совершенно не нужны в готовой игре. (Отметим, однако, что с точки зрения производительности загрузки данных абсолютно никаких показаний к проведению такой оптимизации нет.)
Убрать (или существенно снизить) накладные расходы на гибкость позволяет использование входных данных не для непосредственного создания объектов логики, но для генерации узкоспециализированного кода, создающего эти объекты. При этом одна из возможных стратегий состоит в том, что на этапе активного изменения данных этот код генерируется, компилируется и исполняется налету (благо компиляция в Lua – весьма быстрый процесс), а после стабилизации в дистрибутиве приложения остается и используется только сгенерированный вариант.
Простейший подход к такой кодогенерации – непосредственная замена кода создания объектов в реализации ключевых слов на формирование текста этого кода. При этом все условные переходы переносятся из выполняемого кода в генерирующий, а также (в меру сил) производятся другие оптимизации на основе имеющейся на момент генерации кода информации об объектах и обо всей системе в целом. Если присутствуют какие-либо «декоративные» архитектурные прослойки между генерируемым кодом и конечным системным кодом, их тоже можно попытаться ликвидировать.
Вероятно, наиболее эффективный с точки зрения возможностей оптимизации подход – построение и многопроходный анализ дерева данных, аналогичного дереву, показанному в листинге 3.
Самый простой способ генерации текста (а, значит, и кода) в Lua существует благодаря развитой встроенной поддержке собственного языка регулярных выражений. Вот реализация функции smart_str, позволяющей заполнять строки-шаблоны конкретными значениями (в варианте, не требующем написания скобок):
smart_str = function(str) return function(context)
return string.gsub(str, “($%((.-)%)”,
function(capture)
local c = context[capture]
if not c == nil then error(“Context ‘” .. capture .. “’ not found”) end
return tostring(c)
end
)
end end
Здесь стандартная функция string.gsub используется для замены всех вхождений «$(имя_переменной)» на значение этой переменной в переданной вторым параметром таблице-контексте. Пример использования, печатающий текущую дату:
local str = smart_str “Today is $(date)” { date = os.date() }
print(str)
Подробно возможности встроенного языка регулярных выражений описаны в руководстве по языку Lua.
Иногда бывает необходим больший, чем может дать приведенная реализация smart_str, контроль над кодогенерацией. Например, нужно использовать циклы или условные переходы. Можно пойти по пути усложнения синтаксиса, поддерживаемого этой функцией.
Однако есть другой способ, родственный «наивному» подходу генерации строк, когда код просто «подклеивается» к строке с результатом. Так как строки в Lua неизменяемы, код, подобный следующему, весьма неэффективен (он засоряет память промежуточными значениями result):
local result = ‘return {‘
for i = 1, 1000 do
result = result .. math.random() .. ‘;’
end
result = result .. ‘}’
Значительно более эффективно во время генерации сохранять строки в таблице, а после генерации склеить их все вместе при помощи стандартной функции table.concat. После некоторой доработки получаем второй способ кодогенерации:
local strings = { }
local i = 1
local _ = function(val) strings[i] = tostring(val); i = i + 1 end
_ ‘return {‘
for i = 1, 1000 do
_ (math.random()) _ ‘;’
end
_ ‘}’
local result = table.concat(strings)
В не слишком запущенных случаях этот способ позволяет писать достаточно наглядный код, одинаково хорошо показывающий как шаблон генерируемого кода, так и логику генерации.
Однако, вероятно, в тяжелых случаях со сложной логикой генерации помочь может только реализация «объектной модели» кода Lua (code document object model).
Рассмотрим, как можно организовать генерацию кода для нашего случая с пользовательским интерфейсом. Пойдем по простейшему пути. Запускаем обход от самого нижнего элемента подобно листингу 2. Каждый элемент возвращает строку, которую нужно «подклеить» к общему коду. Функцию process_child_keywords заменяем на функцию concat_child_keywords, конкатенирующую строки, полученные от всех дочерних ключевых слов. Несколько наивная реализация приводится ниже.
concat_child_keywords = function(data)
local strings = { }; local i = 1
for _, child in ipairs(data) do
if type(child) == “function” then
strings[i] = child(data); i = i + 1
end
end
return table.concat(strings)
end
hidden = function(dest) dest.visible = false; return “” end
panel = name_data(“panel”) (function(name, data)
local create_children = concat_child_keywords(data)
if data.visible == nil then data.visible = true end
if data.modal == nil then data.modal = false end
return smart_str (
‘do local wnd=Window.Create($(name),parent_wnd,$(modal));’ ..
‘wnd:SetVisible($(visible))’ ..
‘push_parent_wnd(parent_wnd);’ ..
‘parent_wnd=wnd ush_parent_wnd(wnd);’ ..
‘$(create_children);’ ..
‘parent_wnd=pop_parent_wnd(wnd);end;\n’
) {
name = ‘”’ .. name .. ‘”’;
modal = data.modal;
visible = data.visible;
create_children = create_children;
}
end
end)
Иногда требуется умение загружать и снова сохранять данные, не имея описания их конкретного формата. Как и для XML для нашего случая такое тоже возможно. Данные достаточно легко переводятся в вид, показанный в листинге 3, если обобщить код листинга 2, используя метаметод __call таблицы глобального окружения для получения имени конкретного ключевого слова. (К сожалению, ограничения на объем статьи не позволяют остановиться на этой технике более подробно.)
Сохранение загруженных таким образом данных в тот же формат реализуется как достаточно тривиальная процедура кодогенерации с рекурсивным обходом всего дерева элементов. Может быть удобно создать специальную объектную модель для такого дерева. Это должно облегчить написание кода, который должен модифицировать дерево.
Заключение
Предлагаемый подход – далеко не панацея и не полная замена технологий, основанных на XML. Его главный и определяющий недостаток – малая относительно XML распространенность применяемых технологий, что влечет за собой недостаток информации по методикам работы, недостаток проработанных библиотек для использования Lua в таком ключе и прочее.
Однако, если в вашем проекте еще не реализована полноценная поддержка технологий XML, применение Lua для хранения данных вполне способно получить определенную выгоду. Потенциальная мощность описанного подхода видится сравнимой с XML.
Преимущества такого подхода – наличие «бесплатного» бинарного представления, меньшая избыточность, возможность реализации «самоподнимающихся» данных и т.п. делают его достойным рассмотрения – особенно если вы уже используете Lua в вашем проекте либо у вас есть необходимость во встраивании в проект произвольного скриптового языка.
С другой стороны, малый по сравнению с полнофункциональными библиотеками работы с XML объем и высокая скорость работы виртуальной машины языка Lua может оправдать ее встраивание исключительно с целью использования Lua для хранения данных.
Комментариев нет:
Отправить комментарий