Основы декларативного программирования на 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_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
В декларативном стиле этот код мог бы выглядеть так:
Гораздо нагляднее. Но как сделать, чтобы это работало?
Основы
Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка Луа. Я поверхностно расскажу о самых важных для понимания данной статьи. Более подробную информацию можно получить по ссылкам ниже.
Динамическая типизация
Важно помнить, что Луа — язык с динамической типизацией. Это значит, что тип в языке связан не с переменной, а с её значением. Одна и та же переменная может принимать значения разных типов:
Таблицы
Таблицы (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
end
end
local x5 = make_multiplier ( 5 )
print ( x5 ( 10 ) ) —> 50
Важно помнить, что «объявление функции» в Луа — на самом деле синтаксический сахар, скрывающий создание значения типа «функция» и присвоение его переменной.
Следующие два способа создания функции эквивалентны. Создаётся новая функция и присваивается глобальной переменной mul.
Вызов функции без круглых скобок
В Луа можно не ставить круглые скобки при вызове функции с единственным аргументом, если этот аргумент — строковый литерал или конструктор таблицы. Это очень удобно при написании кода в декларативном стиле.
my_name_is = function ( name )
print ( «Use the force,» , name )
end
my_name_is «Luke» —> Use the force, Luke
shopping_list = function ( items )
print ( «Shopping list:» )
for name, qty in pairs ( items ) do
print ( «*» , qty, «x» , name )
end
end
shopping_list
< milk = 2 ; bread = 1 ; apples = 10 ; >
—> Shopping list:
—> * 2 x milk
—> * 1 x bread
—> * 10 x apples
Цепочки вызовов
Как я уже упоминал, функция в Луа может вернуть другую функцию (или даже саму себя). Возвращённую функцию можно вызвать сразу же:
function chain_print ( . )
print ( . )
return chain_print
end
chain_print ( 1 ) ( «alpha» ) ( 2 ) ( «beta» ) ( 3 ) ( «gamma» )
—> 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 >
myobj [ «foo» ] = function ( self, b )
print ( self [ «a_» ] + b )
end
myobj [ «foo» ] ( myobj, 37 ) —> 42
Примечание: Как можно заметить, при вызове метода без использования двоеточия, myobj упоминается два раза. Следующие два примера, очевидно, не эквивалентны в случае, когда get_myobj() выполняется с побочными эффектами.
Чтобы код был эквивалентен варианту с двоеточием, нужна временная переменная:
При вызове методов через двоеточие также можно опускать круглые скобки, если методу передаётся единственный явный аргумент — строковый литерал или конструктор таблицы:
Реализация
Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:
Что же там написано?
Приведу эквивалентную реализацию без декларативных «выкрутасов»:
do
local tmp_1 = gui : label ( «Hello, world!» )
local label = tmp_1 ( < font_size = 20 >)
local tmp_2 = gui : button ( «OK» )
local button = tmp_2 ( < >)
local tmp_3 = gui : dialog ( «Message Box» )
build_message_box = tmp_3 ( < label, button >)
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 )
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
Теперь gui:dialog() можно записать нагляднее:
gui.dialog = declarative_method ( function ( self, title, element_list )
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 )
Реализация методов gui:label() и gui:button() стала очевидна:
gui.label = declarative_method ( function ( self, text, parameters )
return function ( gui_builder )
local my_label = gui_builder:label ( )
my_label:set_text ( text )
if parameters.font_size then
my_label:set_font_size ( parameters.font_size )
end
return my_label
end
end )
gui.button = declarative_method ( function ( self, title, parameters )
return function ( gui_builder )
local my_button = gui_builder:button ( )
my_button:set_title ( title )
— Так сложилось, что у нашей кнопки нет параметров.
return my_button
end
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-го года, в которой, в частности, хорошо видны истоки декларативного синтаксиса в Луа.