Луа (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_font_size(20)
my_label:set_text("Hello, world!")
my_dialog:add(my_label)
local my_button = gui_builder:button()
my_button:set_title("OK")
my_dialog:add(my_button)
return my_dialog
end
В декларативном стиле этот код мог бы выглядеть так:
Гораздо нагляднее. Но как сделать, чтобы это работало?
Основы
Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка
Луа. Я поверхностно расскажу о самых важных для понимания данной
статьи. Более подробную информацию можно получить по ссылкам ниже.
Динамическая типизация
Важно помнить, что Луа — язык с динамической типизацией. Это значит, что
тип в языке связан не с переменной, а с её значением. Одна и та же
переменная может принимать значения разных типов:
a = "the meaning of life" --> была строка,
a = 42 --> стало число
Таблицы
Таблицы (table) — основное средство композиции данных в Луа. Таблица — это и record и array и dictionary и set и object.
Для программирования на Луа очень важно хорошо знать этот тип данных. Я
кратко остановлюсь лишь на самых важных для понимания деталях.
Создаются таблицы при помощи «конструктора таблиц» (table constructor) — пары фигурных скобок.
Создадим пустую таблицу t:
t = { }
Запишем в таблицу t строку «one» по ключу 1 и число 1 по ключу «one»:
t[1] = "one"
t["one"] = 1
Содержимое таблицы можно указать при её создании:
t = { [1] = "one", ["one"] = 1 }
Таблица в Луа может содержать ключи и значения всех типов (кроме nil).
Но чаще всего в качестве ключей используются целые положительные числа
(array) или строки (record / dictionary). Для работы с этими типами
ключей язык предоставляет особые средства. Я остановлюсь только на
синтаксисе.
Во-первых: при создании таблицы можно опускать положительные
целочисленные ключи для идущих подряд элементов. При этом элементы
получают ключи в том же порядке, в каком они указаны в конструкторе
таблицы. Первый неявный ключ — всегда единица. Явно указанные ключи при
выдаче неявных игнорируются.
Следующие две формы записи эквивалентны:
t = { [1] = "one", [2] = "two", [3] = "three" }
t = { "one", "two", "three" }
При создании таблицы следующие две формы записи эквивалентны:
t = { ["one"] = 1 }
t = { one = 1 }
Аналогично для индексации при записи…
t["one"] = 1
t.one = 1
… И при чтении:
print(t["one"])
print(t.one)
Функции
Функции в Луа — значения первого класса.
Это значит, что функцию можно использовать во всех случаях, что и,
например, строку: присваивать переменной, хранить в таблице в качестве
ключа или значения, передавать в качестве аргумента или возвращаемого
значения другой функции.
Функции в Луа можно создавать динамически в любом месте кода. При этом
внутри функции доступны не только её аргументы и глобальные переменные,
но и локальные переменные из внешних областей видимости. Функции в Луа,
на самом деле, это замыкания (closures).
function make_multiplier(coeff)
return function(value)
return value * coeff
end
end
local x5 = make_multiplier(5)
print(x5(10)) --> 50
Важно помнить, что «объявление функции» в Луа — на самом деле
синтаксический сахар, скрывающий создание значения типа «функция» и
присвоение его переменной.
Следующие два способа создания функции эквивалентны. Создаётся новая функция и присваивается глобальной переменной mul.
С сахаром:
function mul(lhs, rhs) return lhs * rhs end
Без сахара:
mul = function(lhs, rhs) return lhs * rhs end
Вызов функции без круглых скобок
В Луа можно не ставить круглые скобки при вызове функции с единственным аргументом, если этот аргумент — строковый литерал или конструктор таблицы. Это очень удобно при написании кода в декларативном стиле.
Строковый литерал:
my_name_is = function(name)
print("Use the force,", name)
end
my_name_is "Luke" --> Use the force, Luke
Без сахара:
my_name_is("Luke")
Конструктор таблицы:
shopping_list = function(items)
print("Shopping list:")
for name, qty in pairs(items) do
print("*", qty, "x", name)
end
end
В примере выше можно опустить скобки вокруг строковых литералов:
chain_print (1) "alpha" (2) "beta" (3) "gamma"
Для наглядности приведу эквивалентный код без «выкрутасов»:
do
local tmp1 = chain_print(1)
local tmp2 = tmp1("alpha")
local tmp3 = tmp2(2)
local tmp4 = tmp3("beta")
local tmp5 = tmp4(3)
tmp5("gamma")
end
Методы
Объекты в Луа — чаще всего реализуются при помощи таблиц.
За методами, обычно, скрываются значения-функции, получаемые индексированием таблицы по строковому ключу-идентификатору.
Луа предоставляет специальный синтаксический сахар для объявления и
вызова методов — двоеточие. Двоеточие скрывает первый аргумент метода —
self, сам объект.
Следующие три формы записи эквивалентны. Создаётся глобальная переменная
myobj, в которую записывается таблица-объект с единственным методом
foo.
С двоеточием:
myobj = { a_ = 5 }
function myobj:foo(b)
print(self.a_ + b)
end
myobj:foo(37) --> 42
Без двоеточия:
myobj = { a_ = 5 }
function myobj.foo(self, b)
print(self.a_ + b)
end
myobj.foo(myobj, 37) --> 42
Совсем без сахара:
myobj = { ["a_"] = 5 }
myobj["foo"] = function(self, b)
print(self["a_"] + b)
end
myobj["foo"](myobj, 37) --> 42
Примечание: Как можно заметить, при вызове метода без
использования двоеточия, myobj упоминается два раза. Следующие два
примера, очевидно, не эквивалентны в случае, когда get_myobj() выполняется с побочными эффектами.
С двоеточием:
get_myobj():foo(37)
Без двоеточия:
get_myobj().foo(get_myobj(), 37)
Чтобы код был эквивалентен варианту с двоеточием, нужна временная переменная:
do
local tmp = get_myobj()
tmp.foo(tmp, 37)
end
При вызове методов через двоеточие также можно опускать круглые скобки,
если методу передаётся единственный явный аргумент — строковый литерал
или конструктор таблицы:
foo:bar ""
foo:baz { }
Реализация
Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:
В интерфейсе объекта 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_font_size(20)
my_label:set_text("Hello, world!")
my_dialog:add(my_label)
local my_button = gui_builder:button()
my_button:set_title("OK")
my_dialog:add(my_button)
return my_dialog
end
Попробуем представить себе, как мог бы выглядеть метод gui:dialog():
function gui:dialog(title)
return function(element_list)
-- Наша build_message_box():
return function(gui_builder)
local my_dialog = gui_builder:dialog()
my_dialog:set_title(title)
for i = 1, #element_list do
my_dialog:add(
element_list[i](gui_builder)
)
end
return my_dialog
end
end
end
Ситуация с [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)
end
end
end
my_button:set_title(title)
-- Так сложилось, что у нашей кнопки нет параметров.
return my_button
end
end)
Что же у нас получилось?
Проблема улучшения читаемости нашего наивного императивного примера успешно решена.
В результате нашей работы мы, фактически, реализовали с помощью Луа
собственный предметно-ориентированный декларативный язык описания
«игрушечного» пользовательского интерфейса (DSL).
Благодаря особенностям Луа реализация получилась дешёвой и достаточно гибкой и мощной.
В реальной жизни всё, конечно, несколько сложнее. В зависимости от
решаемой задачи нашему механизму могут потребоваться достаточно
серьёзные доработки.
Например, если на нашем микро-языке будут писать пользователи, нам понадобится поместить выполняемый код в песочницу. Также, нужно будет серьёзно поработать над понятностью сообщений об ошибках.
Описанный механизм — не панацея, и применять его нужно с умом как и
любой другой. Но, тем не менее, даже в таком простейшем виде,
декларативный код может сильно повысить читаемость программы и облегчить
жизнь программистам.
Полностью работающий пример можно посмотреть здесь.