Луа (Lua) — мощный, быстрый, лёгкий, расширяемый и встраиваемый скриптовый язык программирования. Луа удобно использовать для написания бизнес-логики приложений.
Отдельные части логики приложения часто бывает удобно описывать в декларативном стиле. Декларативный стиль программирования отличается от более привычного многим императивного тем, что описывается, в первую очередь, каково нечто а не как именно оно создаётся. Написание кода в декларативном стиле часто позволяет скрыть лишние детали реализации.
Луа — мультипарадигменный язык программирования. Одна из сильных сторон Луа — хорошая поддержка декларативного стиля. В этой статье я кратко опишу базовые декларативные средства, предоставлямые языком Луа.
В качестве наивного примера возьмём код создания диалогового окна с текстовым сообщением и кнопкой в императивном стиле:
В декларативном стиле этот код мог бы выглядеть так:
Гораздо нагляднее. Но как сделать, чтобы это работало?
Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка Луа. Я поверхностно расскажу о самых важных для понимания данной статьи. Более подробную информацию можно получить по ссылкам ниже.
Важно помнить, что Луа — язык с динамической типизацией. Это значит, что тип в языке связан не с переменной, а с её значением. Одна и та же переменная может принимать значения разных типов:
Таблицы (table) — основное средство композиции данных в Луа. Таблица — это и record и array и dictionary и set и object.
Для программирования на Луа очень важно хорошо знать этот тип данных. Я кратко остановлюсь лишь на самых важных для понимания деталях.
Создаются таблицы при помощи «конструктора таблиц» (table constructor) — пары фигурных скобок.
Создадим пустую таблицу t:
Запишем в таблицу t строку «one» по ключу 1 и число 1 по ключу «one»:
Содержимое таблицы можно указать при её создании:
Таблица в Луа может содержать ключи и значения всех типов (кроме nil). Но чаще всего в качестве ключей используются целые положительные числа (array) или строки (record / dictionary). Для работы с этими типами ключей язык предоставляет особые средства. Я остановлюсь только на синтаксисе.
Во-первых: при создании таблицы можно опускать положительные целочисленные ключи для идущих подряд элементов. При этом элементы получают ключи в том же порядке, в каком они указаны в конструкторе таблицы. Первый неявный ключ — всегда единица. Явно указанные ключи при выдаче неявных игнорируются.
Следующие две формы записи эквивалентны:
Во-вторых: При использовании строковых литералов в качестве ключей можно опускать кавычки и квадратные скобки, если литерал удовлетворяет ограничениям, налагаемым на луашные идентификаторы.
При создании таблицы следующие две формы записи эквивалентны:
Аналогично для индексации при записи…
… И при чтении:
Функции в Луа — значения первого класса. Это значит, что функцию можно использовать во всех случаях, что и, например, строку: присваивать переменной, хранить в таблице в качестве ключа или значения, передавать в качестве аргумента или возвращаемого значения другой функции.
Функции в Луа можно создавать динамически в любом месте кода. При этом внутри функции доступны не только её аргументы и глобальные переменные, но и локальные переменные из внешних областей видимости. Функции в Луа, на самом деле, это замыкания (closures).
Важно помнить, что «объявление функции» в Луа — на самом деле синтаксический сахар, скрывающий создание значения типа «функция» и присвоение его переменной.
Следующие два способа создания функции эквивалентны. Создаётся новая функция и присваивается глобальной переменной mul.
С сахаром:
Без сахара:
В Луа можно не ставить круглые скобки при вызове функции с единственным аргументом, если этот аргумент — строковый литерал или конструктор таблицы. Это очень удобно при написании кода в декларативном стиле.
Строковый литерал:
Без сахара:
Конструктор таблицы:
Без сахара:
Как я уже упоминал, функция в Луа может вернуть другую функцию (или даже саму себя). Возвращённую функцию можно вызвать сразу же:
В примере выше можно опустить скобки вокруг строковых литералов:
Для наглядности приведу эквивалентный код без «выкрутасов»:
Объекты в Луа — чаще всего реализуются при помощи таблиц.
За методами, обычно, скрываются значения-функции, получаемые индексированием таблицы по строковому ключу-идентификатору.
Луа предоставляет специальный синтаксический сахар для объявления и вызова методов — двоеточие. Двоеточие скрывает первый аргумент метода — self, сам объект.
Следующие три формы записи эквивалентны. Создаётся глобальная переменная myobj, в которую записывается таблица-объект с единственным методом foo.
С двоеточием:
Без двоеточия:
Совсем без сахара:
Примечание: Как можно заметить, при вызове метода без использования двоеточия, myobj упоминается два раза. Следующие два примера, очевидно, не эквивалентны в случае, когда get_myobj() выполняется с побочными эффектами.
С двоеточием:
Без двоеточия:
Чтобы код был эквивалентен варианту с двоеточием, нужна временная переменная:
При вызове методов через двоеточие также можно опускать круглые скобки, если методу передаётся единственный явный аргумент — строковый литерал или конструктор таблицы:
Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:
Приведу эквивалентную реализацию без декларативных «выкрутасов»:
Как мы видим, всю работу выполняет объект gui — «конструктор» нашей функции build_message_box(). Теперь уже видны очертания его интерфейса.
Опишем их в псевдокоде:
В интерфейсе объекта gui чётко виден шаблон — метод, принимающий часть аргументов и возвращающий функцию, принимающую остальные аргументы и возвращающую окончательный результат.
Для простоты, будем считать, что мы надстраиваем декларативную модель поверх существующего API gui_builder, упомянутого в императивном примере в начале статьи. Напомню код примера:
Попробуем представить себе, как мог бы выглядеть метод gui:dialog():
Ситуация с [gui_element] прояснилась. Это — функция-конструктор, создающая соответствующий элемент диалога.
Функция build_message_box() создаёт диалог, вызывает функции-конструкторы для дочерних элементов, после чего добавляет эти элементы к диалогу. Функции-конструкторы для элементов диалога явно очень похожи по устройству на build_message_box(). Генерирующие их методы объекта gui тоже будут похожи.
Напрашивается как минимум такое обобщение:
Теперь gui:dialog() можно записать нагляднее:
Реализация методов gui:label() и gui:button() стала очевидна:
Проблема улучшения читаемости нашего наивного императивного примера успешно решена.
В результате нашей работы мы, фактически, реализовали с помощью Луа собственный предметно-ориентированный декларативный язык описания «игрушечного» пользовательского интерфейса (DSL).
Благодаря особенностям Луа реализация получилась дешёвой и достаточно гибкой и мощной.
В реальной жизни всё, конечно, несколько сложнее. В зависимости от решаемой задачи нашему механизму могут потребоваться достаточно серьёзные доработки.
Например, если на нашем микро-языке будут писать пользователи, нам понадобится поместить выполняемый код в песочницу. Также, нужно будет серьёзно поработать над понятностью сообщений об ошибках.
Описанный механизм — не панацея, и применять его нужно с умом как и любой другой. Но, тем не менее, даже в таком простейшем виде, декларативный код может сильно повысить читаемость программы и облегчить жизнь программистам.
Полностью работающий пример можно посмотреть здесь.
Отдельные части логики приложения часто бывает удобно описывать в декларативном стиле. Декларативный стиль программирования отличается от более привычного многим императивного тем, что описывается, в первую очередь, каково нечто а не как именно оно создаётся. Написание кода в декларативном стиле часто позволяет скрыть лишние детали реализации.
Луа — мультипарадигменный язык программирования. Одна из сильных сторон Луа — хорошая поддержка декларативного стиля. В этой статье я кратко опишу базовые декларативные средства, предоставлямые языком Луа.
Пример
В качестве наивного примера возьмём код создания диалогового окна с текстовым сообщением и кнопкой в императивном стиле:
function build_message_box(gui_builder)<br/>
local my_dialog = gui_builder:dialog()<br/>
my_dialog:set_title("Message Box")<br/>
<br/>
local my_label = gui_builder:label()<br/>
my_label:set_font_size(20)<br/>
my_label:set_text("Hello, world!")<br/>
my_dialog:add(my_label)<br/>
<br/>
local my_button = gui_builder:button()<br/>
my_button:set_title("OK")<br/>
my_dialog:add(my_button)<br/>
<br/>
return my_dialog<br/>
end
В декларативном стиле этот код мог бы выглядеть так:
build_message_box = gui:dialog "Message Box"<br/>
{<br/>
gui:label "Hello, world!" { font_size = 20 };<br/>
gui:button "OK" { };<br/>
}
Гораздо нагляднее. Но как сделать, чтобы это работало?
Основы
Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка Луа. Я поверхностно расскажу о самых важных для понимания данной статьи. Более подробную информацию можно получить по ссылкам ниже.
Динамическая типизация
Важно помнить, что Луа — язык с динамической типизацией. Это значит, что тип в языке связан не с переменной, а с её значением. Одна и та же переменная может принимать значения разных типов:
a = "the meaning of life" --> была строка,<br/>
a = 42 --> стало число
Таблицы
Таблицы (table) — основное средство композиции данных в Луа. Таблица — это и record и array и dictionary и set и object.
Для программирования на Луа очень важно хорошо знать этот тип данных. Я кратко остановлюсь лишь на самых важных для понимания деталях.
Создаются таблицы при помощи «конструктора таблиц» (table constructor) — пары фигурных скобок.
Создадим пустую таблицу t:
t = { }
Запишем в таблицу t строку «one» по ключу 1 и число 1 по ключу «one»:
t[1] = "one"<br/>
t["one"] = 1
Содержимое таблицы можно указать при её создании:
t = { [1] = "one", ["one"] = 1 }
Таблица в Луа может содержать ключи и значения всех типов (кроме nil). Но чаще всего в качестве ключей используются целые положительные числа (array) или строки (record / dictionary). Для работы с этими типами ключей язык предоставляет особые средства. Я остановлюсь только на синтаксисе.
Во-первых: при создании таблицы можно опускать положительные целочисленные ключи для идущих подряд элементов. При этом элементы получают ключи в том же порядке, в каком они указаны в конструкторе таблицы. Первый неявный ключ — всегда единица. Явно указанные ключи при выдаче неявных игнорируются.
Следующие две формы записи эквивалентны:
t = { [1] = "one", [2] = "two", [3] = "three" }<br/>
t = { "one", "two", "three" }
Во-вторых: При использовании строковых литералов в качестве ключей можно опускать кавычки и квадратные скобки, если литерал удовлетворяет ограничениям, налагаемым на луашные идентификаторы.
При создании таблицы следующие две формы записи эквивалентны:
t = { ["one"] = 1 }<br/>
t = { one = 1 }
Аналогично для индексации при записи…
t["one"] = 1<br/>
t.one = 1
… И при чтении:
print(t["one"])<br/>
print(t.one)
Функции
Функции в Луа — значения первого класса. Это значит, что функцию можно использовать во всех случаях, что и, например, строку: присваивать переменной, хранить в таблице в качестве ключа или значения, передавать в качестве аргумента или возвращаемого значения другой функции.
Функции в Луа можно создавать динамически в любом месте кода. При этом внутри функции доступны не только её аргументы и глобальные переменные, но и локальные переменные из внешних областей видимости. Функции в Луа, на самом деле, это замыкания (closures).
function make_multiplier(coeff)<br/>
return function(value)<br/>
return value * coeff<br/>
end<br/>
end<br/>
<br/>
local x5 = make_multiplier(5)<br/>
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)<br/>
print("Use the force,", name)<br/>
end<br/>
<br/>
my_name_is "Luke" --> Use the force, Luke
Без сахара:
my_name_is("Luke")
Конструктор таблицы:
shopping_list = function(items)<br/>
print("Shopping list:")<br/>
for name, qty in pairs(items) do<br/>
print("*", qty, "x", name)<br/>
end<br/>
end<br/>
<br/>
shopping_list<br/>
{<br/>
milk = 2;<br/>
bread = 1;<br/>
apples = 10;<br/>
}<br/>
<br/>
--> Shopping list:<br/>
--> * 2 x milk<br/>
--> * 1 x bread<br/>
--> * 10 x apples
Без сахара:
shopping_list(<br/>
{<br/>
milk = 2;<br/>
bread = 1;<br/>
apples = 10;<br/>
}<br/>
)
Цепочки вызовов
Как я уже упоминал, функция в Луа может вернуть другую функцию (или даже саму себя). Возвращённую функцию можно вызвать сразу же:
function chain_print(...)<br/>
print(...)<br/>
return chain_print<br/>
end<br/>
<br/>
chain_print (1) ("alpha") (2) ("beta") (3) ("gamma")<br/>
--> 1<br/>
--> alpha<br/>
--> 2<br/>
--> beta<br/>
--> 3<br/>
--> gamma
В примере выше можно опустить скобки вокруг строковых литералов:
chain_print (1) "alpha" (2) "beta" (3) "gamma"
Для наглядности приведу эквивалентный код без «выкрутасов»:
do<br/>
local tmp1 = chain_print(1)<br/>
local tmp2 = tmp1("alpha")<br/>
local tmp3 = tmp2(2)<br/>
local tmp4 = tmp3("beta")<br/>
local tmp5 = tmp4(3)<br/>
tmp5("gamma")<br/>
end
Методы
Объекты в Луа — чаще всего реализуются при помощи таблиц.
За методами, обычно, скрываются значения-функции, получаемые индексированием таблицы по строковому ключу-идентификатору.
Луа предоставляет специальный синтаксический сахар для объявления и вызова методов — двоеточие. Двоеточие скрывает первый аргумент метода — self, сам объект.
Следующие три формы записи эквивалентны. Создаётся глобальная переменная myobj, в которую записывается таблица-объект с единственным методом foo.
С двоеточием:
myobj = { a_ = 5 }<br/>
<br/>
function myobj:foo(b)<br/>
print(self.a_ + b)<br/>
end<br/>
<br/>
myobj:foo(37) --> 42
Без двоеточия:
myobj = { a_ = 5 }<br/>
<br/>
function myobj.foo(self, b)<br/>
print(self.a_ + b)<br/>
end<br/>
<br/>
myobj.foo(myobj, 37) --> 42
Совсем без сахара:
myobj = { ["a_"] = 5 }<br/>
<br/>
myobj["foo"] = function(self, b)<br/>
print(self["a_"] + b)<br/>
end<br/>
<br/>
myobj["foo"](myobj, 37) --> 42
Примечание: Как можно заметить, при вызове метода без использования двоеточия, myobj упоминается два раза. Следующие два примера, очевидно, не эквивалентны в случае, когда get_myobj() выполняется с побочными эффектами.
С двоеточием:
get_myobj():foo(37)
Без двоеточия:
get_myobj().foo(get_myobj(), 37)
Чтобы код был эквивалентен варианту с двоеточием, нужна временная переменная:
do <br/>
local tmp = get_myobj()<br/>
tmp.foo(tmp, 37) <br/>
end
При вызове методов через двоеточие также можно опускать круглые скобки, если методу передаётся единственный явный аргумент — строковый литерал или конструктор таблицы:
foo:bar ""<br/>
foo:baz { }
Реализация
Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:
build_message_box = gui:dialog "Message Box"<br/>
{<br/>
gui:label "Hello, world!" { font_size = 20 };<br/>
gui:button "OK" { };<br/>
}
Что же там написано?
Приведу эквивалентную реализацию без декларативных «выкрутасов»:
do<br/>
local tmp_1 = gui:label("Hello, world!")<br/>
local label = tmp_1({ font_size = 20 })<br/>
<br/>
local tmp_2 = gui:button("OK")<br/>
local button = tmp_2({ })<br/>
<br/>
local tmp_3 = gui:dialog("Message Box")<br/>
build_message_box = tmp_3({ label, button })<br/>
end
Интерфейс объекта gui
Как мы видим, всю работу выполняет объект gui — «конструктор» нашей функции build_message_box(). Теперь уже видны очертания его интерфейса.
Опишем их в псевдокоде:
gui:label(title : string)
=> function(parameters : table) : [gui_element]
gui:button(text : string)
=> function(parameters : table) : [gui_element]
gui:dialog(title : string)
=> function(element_list : table) : function
Декларативный метод
В интерфейсе объекта gui чётко виден шаблон — метод, принимающий часть аргументов и возвращающий функцию, принимающую остальные аргументы и возвращающую окончательный результат.
Для простоты, будем считать, что мы надстраиваем декларативную модель поверх существующего API gui_builder, упомянутого в императивном примере в начале статьи. Напомню код примера:
function build_message_box(gui_builder)<br/>
local my_dialog = gui_builder:dialog()<br/>
my_dialog:set_title("Message Box")<br/>
<br/>
local my_label = gui_builder:label()<br/>
my_label:set_font_size(20)<br/>
my_label:set_text("Hello, world!")<br/>
my_dialog:add(my_label)<br/>
<br/>
local my_button = gui_builder:button()<br/>
my_button:set_title("OK")<br/>
my_dialog:add(my_button)<br/>
<br/>
return my_dialog<br/>
end
Попробуем представить себе, как мог бы выглядеть метод gui:dialog():
function gui:dialog(title)<br/>
return function(element_list)<br/>
<br/>
-- Наша build_message_box():<br/>
return function(gui_builder) <br/>
local my_dialog = gui_builder:dialog()<br/>
my_dialog:set_title(title)<br/>
<br/>
for i = 1, #element_list do<br/>
my_dialog:add(<br/>
element_list[i](gui_builder)<br/>
)<br/>
end<br/>
<br/>
return my_dialog <br/>
end<br/>
<br/>
end<br/>
end
Ситуация с [gui_element] прояснилась. Это — функция-конструктор, создающая соответствующий элемент диалога.
Функция build_message_box() создаёт диалог, вызывает функции-конструкторы для дочерних элементов, после чего добавляет эти элементы к диалогу. Функции-конструкторы для элементов диалога явно очень похожи по устройству на build_message_box(). Генерирующие их методы объекта gui тоже будут похожи.
Напрашивается как минимум такое обобщение:
function declarative_method(method)<br/>
return function(self, name)<br/>
return function(data)<br/>
return method(self, name, data)<br/>
end<br/>
end<br/>
end
Теперь gui:dialog() можно записать нагляднее:
gui.dialog = declarative_method(function(self, title, element_list)<br/>
return function(gui_builder) <br/>
local my_dialog = gui_builder:dialog()<br/>
my_dialog:set_title(title)<br/>
<br/>
for i = 1, #element_list do<br/>
my_dialog:add(<br/>
element_list[i](gui_builder)<br/>
)<br/>
end<br/>
<br/>
return my_dialog <br/>
end<br/>
end)
Реализация методов gui:label() и gui:button() стала очевидна:
gui.label = declarative_method(function(self, text, parameters)<br/>
return function(gui_builder) <br/>
local my_label = gui_builder:label()<br/>
<br/>
my_label:set_text(text)<br/>
if parameters.font_size then<br/>
my_label:set_font_size(parameters.font_size)<br/>
end<br/>
<br/>
return my_label<br/>
end<br/>
end)<br/>
<br/>
gui.button = declarative_method(function(self, title, parameters)<br/>
return function(gui_builder) <br/>
local my_button = gui_builder:button()<br/>
<br/>
my_button:set_title(title)<br/>
-- Так сложилось, что у нашей кнопки нет параметров.<br/>
<br/>
return my_button<br/>
end<br/>
end)
Что же у нас получилось?
Проблема улучшения читаемости нашего наивного императивного примера успешно решена.
В результате нашей работы мы, фактически, реализовали с помощью Луа собственный предметно-ориентированный декларативный язык описания «игрушечного» пользовательского интерфейса (DSL).
Благодаря особенностям Луа реализация получилась дешёвой и достаточно гибкой и мощной.
В реальной жизни всё, конечно, несколько сложнее. В зависимости от решаемой задачи нашему механизму могут потребоваться достаточно серьёзные доработки.
Например, если на нашем микро-языке будут писать пользователи, нам понадобится поместить выполняемый код в песочницу. Также, нужно будет серьёзно поработать над понятностью сообщений об ошибках.
Описанный механизм — не панацея, и применять его нужно с умом как и любой другой. Но, тем не менее, даже в таком простейшем виде, декларативный код может сильно повысить читаемость программы и облегчить жизнь программистам.
Полностью работающий пример можно посмотреть здесь.
Дополнительное чтение
- Lua Programming Manual (Перевод)
- Programming in Lua
- Lua Unofficial Frequently Asked Questions
- Lua Programming Gems
- Lua Users Wiki
- The evolution of an extension language: a history of Lua — Статья 2001-го года, в которой, в частности, хорошо видны истоки декларативного синтаксиса в Луа.