как пользоваться lua скриптами
Добавление скриптинга в программу с помощью Lua
Lua это мощный, быстрый, легкий, встраиваемый язык сценариев. С его помощью можно легко и быстро добавить поддержку скриптинга в вашу программу.
Это может понадобиться в тех случаях, когда вы хотите дать возможность пользователям производить самостоятельную донастройку (кастомизацию) вашей программы, когда вы не хотите перекомпилировать весь проект, при внесении каких-либо изменений в логику работы программы, либо хотите разделить работу над движком и работу над логикой между разработчиками (например, при написании игр).
В этой статье, с помощью простой программы, я хочу показать пример встраивания Lua в ваш проект.
Примеров программ, которые используют Lua достаточно много. Далеко не полный список программ, использующих Lua, можно посмотреть здесь Lua Wiki и здесь Wikipedia
Я приведу пример простой программы, которая принимает в качестве аргументов путь к директории, перечисляет файлы и подкаталоги, передает их в скрипт. Скрипт, с помощью регулярных выражений ищет соответствие в путях, передаваемых файлов и, если находит, переименовывает файл согласно определенным правилам.
Пример я создавал в Visual Studio под Windows. Несмотря на это, приведенный код, за исключением нескольких функций (перечисление файлов, переименование файла), специфичных для Windows, после небольшой адаптации будет работать и на других платформах, т.к. Lua является кроссплатформенным языком сценариев.
Начнем с того, что посетим официальный сайт и скачаем Lua для своей платформы. Для Windows подойдет вот этот архив, включающий в себя библиотеки линковщика, динамические библиотеки и заголовочные файлы Lua.
Почему extern «C»? Lua написан на ANSI C, если попытаться включить файлы без extern «C» то мы получим множество ошибок, таких как:
Это вызвано тем, что соглашения о вызовах в C отличаются от соглашений в C++.
Не забудем подключить библиотеку линковщика:
Теперь необходимо объявить и инициализировать экземпляр Lua интерпретатора.
int _tmain( int argc, _TCHAR* argv[])
<
// Инициализируем экземпляр
g_LuaVM = lua_open();
.
// Закрываем
lua_close(g_LuaVM);
>
Теперь нам необходимо объявить и реализовать две функции, которые будут вызываться из Lua. Первая будет искать соответствие имени файла регулярному выражению:
Вторая — переименовывает файл:
Остановимся подробнее на реализации второй:
// В качестве параметров принимает текущий путь к файлу и новое имя файла
// В случае успеха возвращает 1, иначе 0
int LuaRenameFile(lua_State *luaVM)
<
// Получаем число переданных из Lua скрипта аргументов
int argc = lua_gettop(luaVM);
// Количество возвращаемых значений
return 1;
>
strDestination = strSource.substr(0, strSource.rfind(‘\\’) + 1) + strDestination;
int nResult = ( int )::MoveFileEx(strSource.c_str(), strDestination.c_str(), MOVEFILE_REPLACE_EXISTING|MOVEFILE_WRITE_THROUGH);
// Возвращаем в Lua скрипт результат выполнения MoveFileEx
lua_pushnumber(luaVM, nResult);
// Количество возвращаемых значений
return 1;
>
Как видно — ничего сложного: проверяем количество переданных аргументов, проверяем тип аргументов, извлекаем, выполняем переименование файла и возвращаем результат.
Теперь нам необходимо дать Lua знать, о экспортируемых функциях, делается это просто:
lua_register(g_LuaVM, «RenameFile», LuaRenameFile);
lua_register(g_LuaVM, «MatchString», LuaMatchString);
RenameFile и MatchString это имена функций, которые будут «видны» в скрипте.
Создадим скрипт, выполняющий всю работу:
Чтобы совсем стало понятно, привожу кусок кода, который вызывает эту функцию
// Переместить на начало стека функцию onFileFound
lua_getglobal(g_LuaVM, «onFileFound»);
// Поместить следующим элементом в стек путь к найденному файлу (fileName в скрипте)
lua_pushstring(g_LuaVM, strFilePath.c_str());
Осталось только загрузить скрипт из нашей программы:
// Загружаем скрипт
int s = luaL_loadfile(g_LuaVM, szScriptPath);
// Выполняем крипт
s = lua_pcall(g_LuaVM, 0, LUA_MULTRET, 0);
Как видно встраивание Lua скриптинга в программу является, по сути, тривиальной задачей и вместе с тем дает широкое поле для творчества.
Ниже приведен список ресурсов, на которых можно почитать о Lua более подробно.
Использовать Lua c С++ легче, чем вы думаете. Tutorial по LuaBridge
Данная статья — перевод моего туториала, который я изначально писал на английском. Однако этот перевод содержит дополнения и улучшения по сравнению с оригиналом.
Туториал не требует знания Lua, а вот C++ нужно знать на уровне чуть выше базового, но сложного кода здесь нет.
Когда-то я написал статью про использование Lua с C++ с помощью Lua C API. В то время, как написать простой враппер для Lua, поддерживающий простые переменные и функции, не составляет особого труда, написать враппер, который будет поддерживать более сложные вещи (функции, классы, исключения, пространства имён), уже затруднительно.
Врапперов для использования Lua и C++ написано довольно много. С многими из них можно ознакомиться здесь.
Я протестировал многие из них, и больше всего мне понравился LuaBridge. В LuaBridge есть многое: удобный интерфейс, exceptions, namespaces и ещё много всего.
Но начнём по порядку, зачем вообще использовать Lua c С++?
Зачем использовать Lua?
Конфигурационные файлы. Избавление от констант, магических чисел и некоторых define’ов
Данные вещи можно делать и с помощью простых текстовых файлов, но они не так удобны в обращении. Lua позволяет использовать таблицы, математические выражения, комментарии, условия, системные функции и пр. Для конфигурационных файлов это бывает очень полезно.
Например, можно хранить данные в таком виде:
Можно получать системные переменные:
Можно использовать математические выражения для задания параметров:
Скрипты, плагины, расширение функциональности программы
C++ может вызывать функции Lua, а Lua может вызывать функции C++. Это очень мощный функционал, позволяющий вынести часть кода в скрипты или позволить пользователям писать собственные функции, расширяющие функциональность программы. Я использую функции Lua для различных триггеров в игре, которую я разрабатываю. Это позволяет мне добавлять новые триггеры без рекомпиляции и создания новых функций и классов в C++. Очень удобно.
Немного о Lua. Lua — язык с лицензией MIT, которая позволяет использовать его как в некоммерческих, так и в коммерческих приложениях. Lua написан на C, поэтому Lua работает на большинстве ОС, что позволяет использовать Lua в кросс-платформенных приложениях без проблем.
Установка Lua и LuaBridge
Итак, приступим. Для начала скачайте Lua и LuaBridge
Добавьте include папку Lua и сам LuaBridge в Include Directories вашего проекта
Также добавьте lua52.lib в список библиотек для линковки.
Создайте файл script.lua со следующим содержанием:
Добавьте main.cpp (этот код лишь для проверки того, что всё работает, объяснение будет чуть ниже):
Скомпилируйте и запустите программу. Вы должны увидеть следующее:
LuaBridge works!
And here’s our number:42
Примечание: если программа не компилируется и компилятор жалуется на ошибку “error C2065: ‘lua_State’: undeclared identifier” в файле LuaHelpers.h, то вам нужно сделать следующее:
1) Добавьте эти строки в начало файла LuaHelpers.h
2) Измените 460ую строку Stack.h с этого:
А теперь подробнее о том, как работает код.
Включаем все необходимые хэдеры:
Все функции и классы LuaBridge помещены в namespace luabridge, и чтобы не писать «luabridge» множество раз, я использую эту конструкцию (хотя её лучше помещать в те места, где используется сам LuaBridge)
Открываем наш скрипт. Для каждого скрипта не нужно создавать новый lua_State, можно использовать один lua_State для множества скриптов. При этом нужно учитывать коллизию переменных в глобальном нэймспейсе. Если в script1.lua и script2.lua будут объявлены переменные с одинаковыми именами, то могут возникнуть проблемы
Открываем основные библиотеки Lua(io, math, etc.) и вызываем основную часть скрипта (т.е. если в скрипте были прописаны действия в глобальном нэймспейсе, то они будут выполнены)
Создаём объект LuaRef, который может хранить себе всё, что может хранить переменная Lua: int, float, bool, string, table и т.д.
Преобразовать LuaRef в типы C++ легко:
Проверка и исправление ошибок
Но некоторые вещи могут пойти не так, и стоит производить проверку и обработку ошибок. Рассмотрим наиболее важные и часто встречающиеся ошибки
Что, если скрипт Lua не найден?
Что, если переменная не найдена?
Переменная может быть не объявлена, либо её значение — nil. Это легко проверить с помощью функции isNil()
Переменная не того типа, который мы ожидаем получить
Например, ожидается, что переменная имет тип string, тогда можно сделать такую проверку перед тем как делать каст:
Таблицы
Таблицы — это не просто массивы: таблицы — замечательная структура данных, которая позволяет хранить в них переменные Lua любого типа, другие таблицы и ставить ключи разных типов в соответствие значениям и переменным. Таблицы позволяют представлять и получать конфигурационные файлы в красивом и легкочитаемом виде.
Создайте script.lua с таким содержанием:
Код на C++, позволяющий получить данные из этого скрипта:
Вы должны увидеть на экране следующее:
Window v.0.1
width = 400
height = 500
Как видите, можно получать различные элементы таблицы, используя оператор []. Можно писать короче:
Можно также изменять содержимое таблицы:
Это не меняет значение в скрипте, а лишь значение, которое содержится в ходе выполнения программы. Т.е. происходит следующее:
Чтобы сохранить значение, нужно воспользоваться сериализацией таблиц(table serialization), но данный туториал не об этом.
Пусть теперь таблица выглядит так:
Как можно получить значение window.size.w?
Вот так:
Функции
Давайте напишем простую функции на C++
И напишем вот это в скрипте на Lua:
Затем мы регистрируем функцию в C++
Примечание 1: это нужно делать до вызова «luaL_dofile», иначе Lua попытается вызвать необъявленную функцию
Примечание 2: Функции на C++ и Lua могут иметь разные имена
Данный код зарегистрировал функцию в глобальном namespace Lua. Чтобы зарегистрировать его, например, в namespace «game», нужно написать следующий код:
Тогда функцию printMessage в скриптах нужно будет вызывать данным образом:
Пространства имён в Lua не имеют ничего общего с пространствами имён C++. Они скорее используются для логического объединения и удобства.
Теперь вызовем функцию Lua из C++
Вы должны увидеть следующее:
You can still call C++ functions from Lua functions!
Result:9
Разве не замечательно? Не нужно указывать LuaBridge сколько и каких аргументов у функции, и какие значения она возвращает.
Но есть одно ограничение: у одной функции Lua не может быть более 8 аргументов. Но это ограничение легко обойти, передав таблицу, как аргумент.
Если вы передаёте в функцию больше аргументов, чем требуется, LuaBridge молча проигнорирует их. Однако, если что-то пойдёт не так, то LuaBridge сгенерирует исключение LuaException. Не забудьте словить его! Поэтому рекомендуется окружать код блоками try/catch
Вот полный код примера с функциями:
Что? Есть ещё что-то?
Да. Есть ещё несколько замечательных вещей, о которых я напишу в последующих частях туториала: классы, создание объектов, срок жизни объектов… Много всего!
Также рекомендую прочитать этот dev log, в котором я рассказал о том, как использую скрипты в своей игре, практические примеры всегда полезны.
Santa Simplicita
Просто писать о простом — не так и просто…
Lua за 60 минут
Я сентиментальный программист. Иногда я влюбляюсь в языки программирования, и тогда я могу говорить о них часами. Одним из этих часов я поделюсь с вами.
Lua? Что это?
Lua — простой встраиваемый язык (его можно интегрировать с вашими программами, написанными на других языках), легкий и понятный, с одним типом данных, с однообразным синтаксисом. Идеальный язык для изучения.
Зачем?
Lua может вам пригодится:
* если вы геймер (плагины для World of Warcraft и множества других игр)
* если вы пишете игры (очень часто в играх движок пишут на C/C++, а AI — на Lua)
* если вы системный программист (на Lua можно писать плагины для nmap, wireshark, nginx и других утилит)
* если вы embedded-разработчик (Lua очень быстрый, компактный и требует очень мало ресурсов)
Что надо для того, чтобы читать дальше?
1. Научитесь программировать. Хотя бы немного. Не важно на каком языке.
2. Установите Lua. Для этого либо скачайте здесь версию 5.2 (http://www.lua.org/download.html), либо ищите ее в репозиториях. Версия 5.1 тоже пойдет, но знайте, что она очень старая.
Все примеры из статьи запускайте в терминале командой наподобие «lua file.lua».
Первые впечатления
Lua — язык с динамической типизацией (переменные получают типы «на лету» в зависимости от присвоенных значений). Писать на нем можно как в императивном, так и в объектно-ориентированном или функциональном стиле (даже если вы не знаете как это — ничего страшного, продолжайте читать). Вот Hello world на Lua:
Что уже можно сказать о языке:
* однострочные комментарии начинаются с двух дефисов «—»
* скобки и точки-с-запятыми можно не писать
Операторы языка
Набор условных операторов и циклов довольно типичен:
В выражениях можно использовать такие вот операторы над переменными:
= (не-равно, да-да, вместо привычного «!=»)
* конкатенация строк (оператор «..»), напр.: s1=»hello»; s2=»world»; s3=s1..s2
* длина/размер (оператор #): s=»hello»; a = #s (‘a’ будет равно 5).
* получение элемента по индексу, напр.: s[2]
Битовых операций в языке долгое время не было, но в версии 5.2 появилась библиотека bit32, которая их реализует (как функции, не как операторы).
Типы данных
Я вам соврал, когда сказал что у языка один тип данных. Их у него много (как и у каждого серьезного языка):
* nil (ровным счетом ничего)
* булевы числа (true/false)
* числа (numbers) — без деления на целые/вещественные. Просто числа.
* строки — кстати, они очень похожи на строки в паскале
* функции — да, переменная может быть типа «функция»
* поток (thread)
* произвольные данные (userdata)
* таблица (table)
Если с первыми типами все понятно, то что же такое userdata? Вспомним о том, что Lua — язык встраиваемый, и обычно тесно работает с компонентами программ, написанными на других языках. Так вот, эти «чужие» компоненты могут создавать данные под свои нужды и хранить эти данные вместе с lua-объектами. Так вот, userdata — и есть подводная часть айсберга, которая с точки зрения языка lua не нужна, но и просто не обращать внимания на нее мы не можем.
А теперь самое важное в языке — таблицы.
Таблицы
Я вам снова соврал, когда сказал, что у языка 8 типов данных. Можете считать что он один: всё — это таблицы (это, кстати, тоже неправда). Таблица — это очень изящная структура данных, она сочетает в себе свойства массива, хэш-таблицы («ключ»-«значение»), структуры, объекта.
ПОДУМАЙТЕ: чему равно a[2] в случае разреженного массива?
В примере выше таблица ведет себя как массив, но на самом деле — у нас ведь есть ключи (индексы) и значения (элементы массива). И при этом ключами могут быть какие угодно типы, не только числа:
Кстати, раз уж у таблицы есть ключи и значения, то можно в цикле перебрать все ключи и соответствующие им значения:
А как же объекты? О них мы узнаем чуть позже, вначале — о функциях.
Функции
Вот пример обычной функции.
Функции языка позволяют принимать несколько аргументов, и возвращать несколько аргументов. Так аргументы, значения которых не указаны явно, считаются равными nil.
ПОДУМАЙТЕ: зачем может понадобиться возвращать несколько аргументов?
Функции могут принимать переменное количество аргументов:
Поскольку функции — это полноценный тип данных, то можно создавать переменные-функции, а можно передавать функции как аргументы других функций
Объекты = функции + таблицы
Раз мы можем сохранять функции в переменных, то и в полях таблиц тоже сможем. А это уже получаются как-бы-методы. Для тех, кто не знаком с ООП скажу, что основная его польза (по крайней мере в Lua) в том, что функции и данные, с которыми они работают находятся рядом — в пределах одного объекта. Для тех, кто знаком с ООП скажу, что классов здесь нет, а наследование прототипное.
Перейдем к примерам. Есть у нас объект, скажем, лампочка. Она умеет гореть и не гореть. Ну а действия с ней можно сделать два — включить и выключить:
А если лампочку сделать объектом, и функции turn_off и turn_on сделать полями объекта, то получится:
Мы вынуждены передавать сам объект лампочки в качестве первого аргумента, потому что иначе наша функция не узнает с какой именно лампочкой надо работать, чтобы сменить состояние on/off. Но чтобы не быть многословными, в Lua есть сокращенная запись, которую обычно и используют — lamp:turn_on(). Итого, мы уже знаем несколько таких упрощений синтаксиса:
Продолжая говорить о сокращениях, функции можно описывать не только явно, как поля структуры, но и в более удобной форме:
Специальные функции
Некоторые имена функций таблиц (методов) зарезервированы, и они несут особый смысл:
Собственно, мы еще можем заменить функцию print с помощью «print = myfunction», да и много других хакерских дел можно сделать.
Области видимости
Переменные бывают глобальные и локальные. При создании все переменные в Lua являются глобальными.
Для указания локальной области видимости пишут ключевое слово local:
Не забывайте об этом слове.
Обработка ошибок
Часто, если возникают ошибки, надо прекратить выполнение определенной функции. Можно, конечно, сделать множество проверок и вызывать «return», если что-то пошло не так. Но это увеличит объем кода. В Lua используется что-то наподобие исключений (exceptions).
Ошибки порождаются с помощью функции error(x). В качестве аргумента можно передать все, что угодно (то, что имеет отношение к ошибке — строковое описание, числовой код, объект, с которым произошла ошибка и т.д.)
Обычно после этой функции вся программа аварийно завершается. А это надо далеко не всегда. Если вы вызываете функцию, которая может создать ошибку (или ее дочерние функции могут создать ошибку), то вызывайте ее безопасно, с помощью pcall():
Стандартные библиотеки
Стандартных библиотек мало, зато это позволяет запускать Lua где угодно. Подробнее можно получить их список здесь — http://www.lua.org/manual/5.2/manual.html
Нестандартных библиотек много, их можно найти на LuaForge, LuaRocks и в других репозиториях.
Между Lua и не-Lua
ВНИМАНИЕ: эту часть рекомендуется читать людям со знанием языка C.
А если нам недостаточно функциональности стандартных библиотек? Если у нас есть наша программа на C, а мы хотим вызывать ее функции из Lua? Для этого есть очень простой механизм.
Допустим, мы хотим создать свою функцию, которая возвращает случайное число (в Lua есть math.random(), но мы хотим поучиться). Нам придется написать вот такой код на C:
Т.е. Lua предоставляет нам функции для работы с типами данных, для получения аргументов функций и возврата результатов. Функций очень мало, и они довольно простые. Теперь мы собираем нашу библиотеку как динамическую, и можем использовать функцию rand():
А если мы хотим вызывать код, написанный на Lua из наших программ? Тогда наши программы должны создавать виртуальную машину Lua, в которой и будут выполняться Lua-скрипты. Это намного проще:
Вы теперь можете писать на Lua. Если вы узнаете интересные моменты про Lua, которые можно было бы отразить в статье — пишите!
Основы декларативного программирования на Lua
Луа (Lua) — мощный, быстрый, лёгкий, расширяемый и встраиваемый скриптовый язык программирования. Луа удобно использовать для написания бизнес-логики приложений.
Отдельные части логики приложения часто бывает удобно описывать в декларативном стиле. Декларативный стиль программирования отличается от более привычного многим императивного тем, что описывается, в первую очередь, каково нечто а не как именно оно создаётся. Написание кода в декларативном стиле часто позволяет скрыть лишние детали реализации.
Луа — мультипарадигменный язык программирования. Одна из сильных сторон Луа — хорошая поддержка декларативного стиля. В этой статье я кратко опишу базовые декларативные средства, предоставлямые языком Луа.
Пример
В качестве наивного примера возьмём код создания диалогового окна с текстовым сообщением и кнопкой в императивном стиле:
function build_message_box ( gui_builder )
local my_dialog = gui_builder:dialog ( )
my_dialog:set_title ( «Message Box» )
local my_label = gui_builder:label ( )
my_label:set_text ( «Hello, world!» )
local my_button = gui_builder:button ( )
В декларативном стиле этот код мог бы выглядеть так:
build_message_box = gui:dialog «Message Box»
Гораздо нагляднее. Но как сделать, чтобы это работало?
Основы
Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка Луа. Я поверхностно расскажу о самых важных для понимания данной статьи. Более подробную информацию можно получить по ссылкам ниже.
Динамическая типизация
Важно помнить, что Луа — язык с динамической типизацией. Это значит, что тип в языке связан не с переменной, а с её значением. Одна и та же переменная может принимать значения разных типов:
Таблицы
Таблицы (table) — основное средство композиции данных в Луа. Таблица — это и record и array и dictionary и set и object.
Для программирования на Луа очень важно хорошо знать этот тип данных. Я кратко остановлюсь лишь на самых важных для понимания деталях.
Создаются таблицы при помощи «конструктора таблиц» (table constructor) — пары фигурных скобок.
Создадим пустую таблицу t:
Запишем в таблицу t строку «one» по ключу 1 и число 1 по ключу «one»:
Содержимое таблицы можно указать при её создании:
Таблица в Луа может содержать ключи и значения всех типов (кроме nil). Но чаще всего в качестве ключей используются целые положительные числа (array) или строки (record / dictionary). Для работы с этими типами ключей язык предоставляет особые средства. Я остановлюсь только на синтаксисе.
Во-первых: при создании таблицы можно опускать положительные целочисленные ключи для идущих подряд элементов. При этом элементы получают ключи в том же порядке, в каком они указаны в конструкторе таблицы. Первый неявный ключ — всегда единица. Явно указанные ключи при выдаче неявных игнорируются.
Следующие две формы записи эквивалентны:
Во-вторых: При использовании строковых литералов в качестве ключей можно опускать кавычки и квадратные скобки, если литерал удовлетворяет ограничениям, налагаемым на луашные идентификаторы.
При создании таблицы следующие две формы записи эквивалентны:
Аналогично для индексации при записи…
Функции
Функции в Луа — значения первого класса. Это значит, что функцию можно использовать во всех случаях, что и, например, строку: присваивать переменной, хранить в таблице в качестве ключа или значения, передавать в качестве аргумента или возвращаемого значения другой функции.
Функции в Луа можно создавать динамически в любом месте кода. При этом внутри функции доступны не только её аргументы и глобальные переменные, но и локальные переменные из внешних областей видимости. Функции в Луа, на самом деле, это замыкания (closures).
function make_multiplier ( coeff )
return function ( value )
return value * coeff
local x5 = make_multiplier ( 5 )
Важно помнить, что «объявление функции» в Луа — на самом деле синтаксический сахар, скрывающий создание значения типа «функция» и присвоение его переменной.
Следующие два способа создания функции эквивалентны. Создаётся новая функция и присваивается глобальной переменной mul.
function mul ( lhs, rhs ) return lhs * rhs end
mul = function ( lhs, rhs ) return lhs * rhs end
Вызов функции без круглых скобок
В Луа можно не ставить круглые скобки при вызове функции с единственным аргументом, если этот аргумент — строковый литерал или конструктор таблицы. Это очень удобно при написании кода в декларативном стиле.
print ( «Shopping list:» )
for name, qty in pairs ( items ) do
Цепочки вызовов
Как я уже упоминал, функция в Луа может вернуть другую функцию (или даже саму себя). Возвращённую функцию можно вызвать сразу же:
chain_print ( 1 ) ( «alpha» ) ( 2 ) ( «beta» ) ( 3 ) ( «gamma» )
В примере выше можно опустить скобки вокруг строковых литералов:
chain_print ( 1 ) «alpha» ( 2 ) «beta» ( 3 ) «gamma»
Для наглядности приведу эквивалентный код без «выкрутасов»:
local tmp1 = chain_print ( 1 )
local tmp2 = tmp1 ( «alpha» )
local tmp3 = tmp2 ( 2 )
local tmp4 = tmp3 ( «beta» )
local tmp5 = tmp4 ( 3 )
Методы
Объекты в Луа — чаще всего реализуются при помощи таблиц.
За методами, обычно, скрываются значения-функции, получаемые индексированием таблицы по строковому ключу-идентификатору.
Луа предоставляет специальный синтаксический сахар для объявления и вызова методов — двоеточие. Двоеточие скрывает первый аргумент метода — self, сам объект.
Следующие три формы записи эквивалентны. Создаётся глобальная переменная myobj, в которую записывается таблица-объект с единственным методом foo.
function myobj:foo ( b )
function myobj.foo ( self, b )
myobj [ «foo» ] = function ( self, b )
print ( self [ «a_» ] + b )
Примечание: Как можно заметить, при вызове метода без использования двоеточия, myobj упоминается два раза. Следующие два примера, очевидно, не эквивалентны в случае, когда get_myobj() выполняется с побочными эффектами.
Чтобы код был эквивалентен варианту с двоеточием, нужна временная переменная:
local tmp = get_myobj ( )
При вызове методов через двоеточие также можно опускать круглые скобки, если методу передаётся единственный явный аргумент — строковый литерал или конструктор таблицы:
Реализация
Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:
build_message_box = gui:dialog «Message Box»
Что же там написано?
Приведу эквивалентную реализацию без декларативных «выкрутасов»:
local tmp_1 = gui : label ( «Hello, world!» )
local tmp_2 = gui : button ( «OK» )
local button = tmp_2 ( < >)
local tmp_3 = gui : dialog ( «Message Box» )
Интерфейс объекта gui
Как мы видим, всю работу выполняет объект gui — «конструктор» нашей функции build_message_box(). Теперь уже видны очертания его интерфейса.
Опишем их в псевдокоде:
Декларативный метод
В интерфейсе объекта gui чётко виден шаблон — метод, принимающий часть аргументов и возвращающий функцию, принимающую остальные аргументы и возвращающую окончательный результат.
Для простоты, будем считать, что мы надстраиваем декларативную модель поверх существующего API gui_builder, упомянутого в императивном примере в начале статьи. Напомню код примера:
function build_message_box ( gui_builder )
local my_dialog = gui_builder:dialog ( )
my_dialog:set_title ( «Message Box» )
local my_label = gui_builder:label ( )
my_label:set_text ( «Hello, world!» )
local my_button = gui_builder:button ( )
Попробуем представить себе, как мог бы выглядеть метод gui:dialog():
return function ( element_list )
return function ( gui_builder )
local my_dialog = gui_builder:dialog ( )
element_list [ i ] ( gui_builder )
Ситуация с [gui_element] прояснилась. Это — функция-конструктор, создающая соответствующий элемент диалога.
Функция build_message_box() создаёт диалог, вызывает функции-конструкторы для дочерних элементов, после чего добавляет эти элементы к диалогу. Функции-конструкторы для элементов диалога явно очень похожи по устройству на build_message_box(). Генерирующие их методы объекта gui тоже будут похожи.
Напрашивается как минимум такое обобщение:
function declarative_method ( method )
return function ( self, name )
return function ( data )
return method ( self, name, data )
Теперь gui:dialog() можно записать нагляднее:
gui.dialog = declarative_method ( function ( self, title, element_list )
return function ( gui_builder )
local my_dialog = gui_builder:dialog ( )
element_list [ i ] ( gui_builder )
Реализация методов gui:label() и gui:button() стала очевидна:
gui.label = declarative_method ( function ( self, text, parameters )
return function ( gui_builder )
local my_label = gui_builder:label ( )
if parameters.font_size then
gui.button = declarative_method ( function ( self, title, parameters )
return function ( gui_builder )
local my_button = gui_builder:button ( )
— Так сложилось, что у нашей кнопки нет параметров.
Что же у нас получилось?
Проблема улучшения читаемости нашего наивного императивного примера успешно решена.
В результате нашей работы мы, фактически, реализовали с помощью Луа собственный предметно-ориентированный декларативный язык описания «игрушечного» пользовательского интерфейса (DSL).
Благодаря особенностям Луа реализация получилась дешёвой и достаточно гибкой и мощной.
В реальной жизни всё, конечно, несколько сложнее. В зависимости от решаемой задачи нашему механизму могут потребоваться достаточно серьёзные доработки.
Например, если на нашем микро-языке будут писать пользователи, нам понадобится поместить выполняемый код в песочницу. Также, нужно будет серьёзно поработать над понятностью сообщений об ошибках.
Описанный механизм — не панацея, и применять его нужно с умом как и любой другой. Но, тем не менее, даже в таком простейшем виде, декларативный код может сильно повысить читаемость программы и облегчить жизнь программистам.
Полностью работающий пример можно посмотреть здесь.