29 сент. 2007 г.
24 сент. 2007 г.
15 сент. 2007 г.
Использование декораторов в Lua
Рассмотрим простейший пример того, что можно сделать при помощи декораторов:
local foo =
print_start_end(printor 'begin foo', printor 'end foo')
..
function (buzz)
print ('FOO called with ' .. buzz)
end
foo('ARGUMENT')
Вывод:
begin foo
FOO called with ARGUMENT
end foo
Реализация:
function printor(msg)
return function() print(msg) end
end
function print_start_end(prologue, epilogue)
assert(type(prologue) == 'function'
and type(epilogue) == 'function')
local M = {}
setmetatable(M, {
__concat = function(lhs, rhs)
assert(type(rhs) == 'function')
return function(...)
prologue()
local result = rhs(...)
epilogue()
return result
end
end;
})
return M
end
Главное достоинство такого метода — в «удобоваримом» синтаксисе без необходимости привлечения метамеханизмов.
Правда, используемый подход не позволяет пользоваться синтаксическим сахаром для объявления функций и методов, но это небольшая беда. Вот к такой записи не прикрутишь декоратор:
function bar(buzz)
print('BAR called with ' .. buzz)
end
foobar = {}
function foobar:buzz(foo)
print('foobar:buzz called with ' .. foo)
end
Один из вариантов применения такого декоратора — проверка входных и выходных параметров функции (и, шире, пред- и постусловий вообще).
Например (первое, что пришло в голову):
local string.rep =
docs
[[Returns a string that is the concatenation
of n copies of the string s.]] ..
args { 'string', optional 'number' } ..
rets { 'string' } ..
function (s, n)
n = n or 1
if n > 1 then
local t = {}
for i = 1, n do
t[i] = s
end
s = table.concat(t)
end
return s
end
Но об этом — в следующий раз.
11 сент. 2007 г.
Closing over context still not easy in mainstream languages?
"Lisp 1.5, complete with support for lexical closures, appeared in 1959. It’s 2007. That’s forty-eight years."
http://www.lshift.net/blog/2007/09/11/closing-over-context-still-not-easy-in-mainstream-languages-film-at-11
Use Lua, man, use Lua!..
7 сент. 2007 г.
It Was a Very Good Year
When I was seventeen
It was a very good year
It was a very good year for small town girls
And soft summer nights
Wed hide from the lights
On the village green
When I was seventeen
When I was twenty-one
It was a very good year
It was a very good year for city girls
Who lived up the stair
With all that perfumed hair
And it came undone
When I was twenty-one
When I was thirty-five
It was a very good year
It was a very good year for blue-blooded girls
Of independent means
Wed ride in limousines
Their chauffeurs would drive
When I was thirty-five
But now the days grow short
Im in the autumn of the year
And now I think of my life as vintage wine
>from fine old kegs
>from the brim to the dregs
And it poured sweet and clear
It was a very good year
It was a mess of good years
Одиннадцатого февраля 2006
5 сент. 2007 г.
Мокрые яблоки
Реализация языков описания данных на основе языка Lua. Часть 5
Кодогенерация
Мы видим, что с увеличением сложности структуры данных усложняется и реализация ключевых слов, и сопутствующего им кода. Растут накладные расходы по производительности загрузки данных. В последнем примере требуется, как минимум, пять вложенных вызовов функций, прежде чем дело доходит до непосредственного создания объекта. В действительности это не очень страшно – виртуальная машина 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 для хранения данных.
Реализация языков описания данных на основе языка 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 не позволяет эффективно обнаружить один из видов ошибок в данных – когда пользователь указывает недостаточное число аргументов для ключевого слова. Например, если для панели указано только имя:
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)
Реализация языков описания данных на основе языка Lua. Часть 3
Как же можно добиться того, чтобы это работало?
Начнем с того, что в функции в Lua – значения первого класса (first class values). Это, в частности, значит, что функции можно присваивать переменным, передавать функциям в качестве параметров и использовать в качестве возвращаемых значений функций.
От своего предка, языка описания данных Sol, Lua унаследовал некоторые особенности синтаксиса, позволяющие этому коду выполняться. Здесь используется то, что Lua позволяет опускать скобки при передаче функции в качестве единственного параметра строкового литерала либо конструктора таблицы. Таким образом, с точки зрения языка, в коде, приведенном выше, вокруг каждой пары кавычек и каждой пары фигурных скобок стоят «неявные» круглые скобки.
Конструкция
foo “string” { key = “value” }эквивалентна конструкции
foo(“string”)({ key = “value” })
Сначала вызывается функция foo, которой передается параметр “string”. Функция foo возвращает другую (анонимную) функцию, которая вызывается с параметром-таблицей { key = “value” }. Например, реализованная следующим образом foo в этом случае выведет на экран «string value»:
foo = function(str)
return function(tab) print(str, tab[“key”]) end
end
Перечисленные полезные особенности Lua предоставляют нам достаточно широкие возможности для реализации «самозагружающихся» описаний данных. Благодаря тому, что Lua позволяет компилировать программы в байт-код для виртуальной машины, мы можем «бесплатно» получить бинарное представление данных с еще меньшей избыточностью, к тому же, снимающее затраты на парсинг и компиляцию текста (впрочем в неэкстремальных случаях затраты на компиляцию и так достаточно невелики). Кроме того, текущая реализация языка Lua легко справляется с большими объемами данных – «пережевать» файл в несколько мегабайт для нее не большая проблема (конечно, при адекватной реализации пресловутой системы «самозагрузки»).
Рис. 3. Порядок вызовов при загрузке данныхЕсли при выполнении кода из листинга 1 в области действия будут находиться реализованные должным образом функции gui_layout, panel, button и checkbox, мы получим набор вызовов, показанный на рис. 3. Переменные modal и hidden могут содержать значения любого типа, главное, чтобы они были тоже видимы.
Фактически можно сказать, что мы видим здесь данные, записанные при помощи языка описания данных (data description language, DDL). Можно сказать, что перечисленные имена переменных – ключевые слова (keywords) в создаваемом нами языке описания пользовательского интерфейса. Нужно заметить, что наши данные носят ярко выраженный иерархический характер.
Поскольку описание данных – валидный код на Lua, мы имеем возможность, например, «разбавить» декларативное описание интерфейса кодом описания его функциональности – скажем, описать обработчики событий по клику на кнопку:
panel “mainmenu”
{
modal;
button ”newgame” { action = StartNewGame; };
button ”settings” { action = function() showpanel(“settings”) end; };
button “exit” { action = confirm(ExitGame); };
};
Конечно, если код обработчика пишется вручную, такой подход следует применять с осторожностью, всячески ограничивая API, доступный таким функциям, и проводя максимальную проверку на их корректность и безопасность.
Помимо этого, наличие функций непосредственно в описании данных несколько затрудняет вариант реализации DDL с промежуточной кодогенерацией (см. ниже) – такие обработчики придется как-то сохранять вместе со всеми данными. Например, их можно сохранять в виде байт-кода при помощи системной функции string.dump –накладываемое ограничение на отсутствие у таких функций ссылок на внешне переменные (upvalues) в данном случае не слишком существенно.
Как же получить загруженные данные? Можно реализовать функции – ключевые слова нашего DDL таким образом, чтобы они изменяли некую глобальную сущность. Однако это повышает связанность кода и уменьшает его гибкость.
Язык Lua позволяет явным образом возвращать значение (или даже несколько значений) при загрузке файла. Для этого достаточно в его конце написать выражение, использующее ключевое слово return так же, как если бы это была обычная функция. Удобно реализовывать корневую функцию иерархии (в нашем случае gui_layout) так, чтобы она возвращала собранные в процессе выполнения остального кода данные (либо сгенерированную функцию, выполняющую некие действия на основе этих данных). Тогда, если дописать в начало листинга 1 ключевое слово return, можно будет добраться до загруженных данных, используя примерно такую конструкцию:
local data = assert(loadfile(“myguilayout.lua”))()
В переменную data будет помещено значение, возвращенное корневой функцией gui_layout. Для большей изящности ключевое слово return можно «подклеивать» автоматически перед загрузкой данных. В этом случае вместо loadfile удобнее использовать loadstring.
Очевидно, помимо реализации необходимого набора ключевых слов, мы должны ограничить пользователя в написании кода на Lua в файлах с данными только этим набором. Без дополнительного разбора текста описания данных невозможно запретить использование в нем произвольного кода, но это и не обязательно. Главное – не дать такому коду доступа к окружающему миру, как говорится, выполнять его в «песочнице» (sandbox). Тогда «лишний» код может быть как минимум бесполезен (но не вреден), но как максимум его можно использовать во благо – например, для генерации данных на лету.
Строго говоря, еще нужно учитывать возможность подвесить загрузку данных, написав в файле с данными бесконечный цикл. Если требуется стопроцентная гарантия от вредоносного кода, необходимо контролировать процесс загрузки, например при помощи установки хуков на исполнение кода в функции debug.sethook, либо при помощи установки вотчдога в отдельном потоке.
К счастью, язык Lua предоставляет эффективное средство для создания такой «песочницы» – возможность явного задания таблицы глобального окружения (environment table) для функций при помощи стандартной функции setfenv. При загрузке данных в глобальное окружение необходимо поместить только функции – ключевые слова нашего DDL.
Функция loadfile возвращает функцию, содержащую весь код загружаемого файла. Глобальное окружение используется по умолчанию для всего этого кода. Заменив это окружение до вызова функции, можно заменить окружение, которое будет назначено коду в файле. Можно написать следующую функцию для загрузки файлов с данными:
load_ddl_file = function(name, ddl_keywords)
local loader = assert(loadfile(name))
setfenv(loader, ddl_keywords)
return loader()
end
Использовать эту функцию можно аналогично loadfile:
local data = assert(load_ddl_file(“myguilayout.lua”, ddl_keywords))
Подразумевается, что реализация ключевых слов языка находится в таблице ddl_keywords. Для большего контроля, можно запретить запись в эту таблицу и чтение из нее данных по отсутствующим ключам, переопределив в ее метатаблице методы __index и __newindex.
Используя при загрузке различные таблицы глобального окружения с реализациями ключевых слов, можно добиться различной интерпретации загружаемых данных, в контексте нашего примера – от генерации для художников спецификаций требуемых графических ресурсов, написанных естественным языком, до использования различных библиотек пользовательского интерфейса в зависимости от системы, на которой запускается приложение.
Учитывая все вышесказанное, можно, наконец, перейти к обсуждению путей реализации самих функций – ключевых слов для нашего примера.