5 сент. 2007 г.

Реализация языков описания данных на основе языка Lua. Часть 4

Предыдущая часть

Содержание

Наконец-то реализация

Большинство ключевых слов, как мы видели выше, требует двух вызовов для получения всех данных. В первом вызове обычно передается имя, во втором – таблица с данными. Чтобы облегчить реализацию, введем вспомогательную функцию – генератор «сборщиков данных». Функция-сборщик собирает переданные ей данные, одновременно проверяя соответствие их типов заданным. После того как нужное количество аргументов собрано, они передаются пользовательской функции (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 не позволяет эффективно обнаружить один из видов ошибок в данных – когда пользователь указывает недостаточное число аргументов для ключевого слова. Например, если для панели указано только имя:

panelbad_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)

Комментариев нет: