Содержание
Наконец-то реализация
Большинство ключевых слов, как мы видели выше, требует двух вызовов для получения всех данных. В первом вызове обычно передается имя, во втором – таблица с данными. Чтобы облегчить реализацию, введем вспомогательную функцию – генератор «сборщиков данных». Функция-сборщик собирает переданные ей данные, одновременно проверяя соответствие их типов заданным. После того как нужное количество аргументов собрано, они передаются пользовательской функции (sink):
pipeline = function(name, ...)
assert(type(name) == “string”)
local types = {...}
local nargs = #types
return function(sink)
local args = { }
local n = nargs
local collector
collector = function(a)
local i = 1 + nargs – n
local argt = types[i]
if argt ~= “any” and type(a) ~= argt then
error(string.format(
"%s: Bad argument %d, %s expected"
name, i, types[i]
))
end
args[i] = a
if n > 1 then
n = n - 1
return collector
else
return sink(unpack(args))
end
end
return collector
end
end
Далее зададим функции для генерации «сборщиков» аргументов заданных фиксированных типов (произвольный тип аргумента можно указать, используя в качестве имени типа слово «any»). Например, для ключевых слов, требующих аргументы по схеме «имя – данные» подойдет такая функция:
name_data = function(name) return pipeline(name, “string”, “table”) end
Наиболее прямолинейный способ реализации ключевых слов – просто собирать все данные в одну большую иерархическую таблицу, занося «имена» и «типы» элементов интерфейса в служебные поля таблиц данных этих элементов. Фактически – это прямое, без каких-либо дополнительных действий, преобразование «исполнимого кода», описывающего данные в сами данные.
Ключевые слова без аргументов в этом и в остальных рассматриваемых случаях – функции, задающие значение определенного ключа в таблице данных. Такие функции играют повышающего наглядность роль «синтаксического сахара». Например (dest – таблица данных для изменения):
hidden = function(dest) dest.visible = false end
Все ключевые слова без аргументов попадают в порядке упоминания в списочную часть таблицы данных ключевого слова-родителя. Значения, возвращенные «дочерними» ключевыми словами также попадают в списочную часть. Для единообразия эти значения тоже можно сделать функциями, которые при вызове будут добавлять данные об элементе-потомке в специальный список, хранящийся в таблице данных элемента-родителя.
Перед анализом данных родительского элемента следует сделать обход списочной части для сбора данных о потомках:
process_child_keywords = function(data)
for key_, child in ipairs(data) do
if type(child) == “function” then
data[key] = child(data)
end
end
end
Все параметризованные функции – ключевые слова кроме корневого можно реализовать по следующей схеме:
Листинг 2
-- Вместо keyword_name нужно подставить имя ключевого слова
keyword_name = name_data(“keyword_name”) (function(name, data)
process_child_keywords(data)
data.type_ = keyword_name
data.name_ = name
return function(dest)
if not dest.children_ then dest.children_ = { } end
table.insert(dest.children_, data)
end
end)
В реализации унарных ключевых слов, очевидно, нужно опустить упоминание name_data, поскольку единственный аргумент передается за один вызов функции.
У корневого ключевое слова (gui_layout) не может быть предков, поэтому должно возвращать готовую таблицу с данными. В остальном оно реализуется по той же схеме:
gui_layout= name_data(“gui_layout”) (function(name, data)
process_child_keywords(data)
data.type_ = gui_layout
data.name_ = name
return data
end)
Приведенная выше реализация функции pipeline не позволяет эффективно обнаружить один из видов ошибок в данных – когда пользователь указывает недостаточное число аргументов для ключевого слова. Например, если для панели указано только имя:
panel “bad_panel”;
Как видно из его реализации, сборщик аргументов collector возвращает самого себя до тех пор, пока не наберет нужного числа аргументов. Поэтому в такой ситуации в списочной части таблицы данных ключевого слова-предка оказывается не результат работы функции – реализации ключевого слова-потомка sink, а сам сборщик.
Эту ошибку можно обнаружить, сделав сборщика из функции таблицей с заданным метаметодом __call (реализацией этого метаметода будет нынешнее тело функции-сборщика) и заданным (мета)полем, обозначающим, что это – сборщик. В process_child_keywords при обнаружении таблицы вместо функции необходимо проверять значение этого поля. Если обнаружен сборщик, нужно выдавать ошибку о недостаточном количестве аргументов. Дополнительную отладочную информацию о природе сборщика (имя, типы, число аргументов) можно хранить в его же (мета)таблице.
Тем же способом можно реализовать задание значений аргументов по умолчанию – вместо выдачи ошибки нужно просто вызывать сборщик с этим значением (или значениями) до тех пор, пока он не наберет достаточное количество аргументов.
//~ Если нужна поддержка значений параметров по умолчанию, их установку целесообразно вставить после обработки ключевых слов-потомков. Например, панели должны быть видимы по умолчанию, значит, для ключевого слова panel после вызова process_child_keywords нужно добавить строчку:
if data.visible == nil then data.visible = true end
На выходе для данных из листинга 1 будет сгенерированна примерно такая таблица (для экономии места описание панели «settings» пропущено):
Листинг 3
{
type_ = “gui_layout”;
name_ = “gamegui”;
children_ = {
[1] = {
type_ = “panel”;
name_ = “mainmenu”;
modal = true;
visible = true;
children_ = {
[1] = { type_ = “button”; name_ = ”newgame”; };
[2] = { type_ = “button”; name_ = ”settings”; };
[3] = { type_ = “button”; name_ = ”exit”; };
};
};
[2] = { type_ = “panel”; name_ = “settings”; … };
};
};
«Самозагружающиеся» данные
Уже в описанном виде с данными можно эффективно работать. Однако это не всегда удобно – если сохраненные данные соответствовали каким-то «живым» объектам логики приложения, придется писать отдельный код по созданию и настройке этих объектов на основе получившихся таблиц. Было бы удобнее создавать объекты сразу же.
Это вполне возможно. Один из путей – встроить код создания объектов логики в функции-модификаторы, возвращаемые дочерними ключами, и изменить логику обхода потомков. Если потомку нужна информация о предке, нужно делать обход потомков не сразу при вызове функции – ключевого слова, как сделано выше, а начиная с корневого элемента. При этом нужно дополнительно передавать модификаторам данные о предке.
Пример для случая, когда для создания нашего элемента пользовательского интерфейса нужна информация о родительском окне:
panel = name_data(“panel”) (function(name, data)
return function(datadest, parent_wnd)
local wnd = Window.Create(name, parent_wnd)
process_child_keywords(data, wnd)
end
end)
Подразумевается, что функция process_child_keywords была модифицирована, чтобы передавать вместе с данными предка еще один параметр – его окно. Нужно модифицировать и реализацию ключевых слов без параметров. При таком подходе они должны уметь изменять свойства самого окна – изменять данные уже поздно. Например, для hidden:
hidden = function(data, wnd) wnd:SetVisible(false) end
Если параметр, изменяемый ключевым словом без аргумента, должен быть доступен на этапе до того, как возможна обработка всех потомков (например, если в приведенном примере нужно передать модальность окна в Window.Create) – необходимо реализовать тем или иным способом обход потомков в два прохода. Самый простой способ – вложить функцию – модификатор параметризованного ключевого слова в еще одну такую же и вызывать process_child_keywords дважды (такую реализацию можно абстрагировать подобно name_data):
panel = name_data(“panel”) (function(name, data)
return function(data)
return function(datadest, parent_wnd)
process_child_keywords(datadest2) – Data modifiers
local wnd = Window.Create(name, parent_wnd, data.modal)
process_child_keywords(data, wnd) -- Child windows
end
end
end)
Комментариев нет:
Отправить комментарий