Главная | Регистрация | Вход | RSSВоскресенье, 15.06.2025, 22:06

НеПотеряйка

Меню сайта
Наш опрос
Что для Вас "МОДЕРНИЗАЦИЯ ОБРАЗОВАНИЯ"?
Всего ответов: 210
Статистика

Онлайн всего: 1
Гостей: 1
Пользователей: 0

Дневник

Главная » 2012 » Март » 02 » Основы декларативного программирования на Lua
Основы декларативного программирования на Lua
22:49

Основы декларативного программирования на Lua

http://habrahabr.ru/blogs/development/77413/#habracut

Луа (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

В декларативном стиле этот код мог бы выглядеть так:

build_message_box = gui:dialog "Message Box"
{
  gui:label "Hello, world!" { font_size = 20 };
  gui:button "OK" { };
}

Гораздо нагляднее. Но как сделать, чтобы это работало?

Основы


Чтобы разобраться в чём дело, нужно знать о некоторых особенностях языка Луа. Я поверхностно расскажу о самых важных для понимания данной статьи. Более подробную информацию можно получить по ссылкам ниже.

Динамическая типизация


Важно помнить, что Луа — язык с динамической типизацией. Это значит, что тип в языке связан не с переменной, а с её значением. Одна и та же переменная может принимать значения разных типов:

= "the meaning of life" --> была строка,
= 42                    --> стало число

Таблицы


Таблицы (table) — основное средство композиции данных в Луа. Таблица — это и record и array и dictionary и set и object.

Для программирования на Луа очень важно хорошо знать этот тип данных. Я кратко остановлюсь лишь на самых важных для понимания деталях.

Создаются таблицы при помощи «конструктора таблиц» (table constructor) — пары фигурных скобок.

Создадим пустую таблицу t:

= { }

Запишем в таблицу t строку «one» по ключу 1 и число 1 по ключу «one»:

t[1] = "one"
t["one"] = 1

Содержимое таблицы можно указать при её создании:

= { [1] = "one"["one"] = 1 }

Таблица в Луа может содержать ключи и значения всех типов (кроме nil). Но чаще всего в качестве ключей используются целые положительные числа (array) или строки (record / dictionary). Для работы с этими типами ключей язык предоставляет особые средства. Я остановлюсь только на синтаксисе.

Во-первых: при создании таблицы можно опускать положительные целочисленные ключи для идущих подряд элементов. При этом элементы получают ключи в том же порядке, в каком они указаны в конструкторе таблицы. Первый неявный ключ — всегда единица. Явно указанные ключи при выдаче неявных игнорируются.

Следующие две формы записи эквивалентны:

= { [1] = "one"[2] = "two"[3] = "three" }
= { "one""two""three" }

Во-вторых: При использовании строковых литералов в качестве ключей можно опускать кавычки и квадратные скобки, если литерал удовлетворяет ограничениям, налагаемым на луашные идентификаторы.

При создании таблицы следующие две формы записи эквивалентны:

= { ["one"] = 1 }
= { 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
 
shopping_list
{
  milk = 2;
  bread = 1;
  apples = 10;
}
 
--> Shopping list:
--> * 2 x milk
--> * 1 x bread
--> * 10 x apples

Без сахара:

shopping_list(
      {
        milk = 2;
        bread = 1;
        apples = 10;
      }
  )

Цепочки вызовов


Как я уже упоминал, функция в Луа может вернуть другую функцию (или даже саму себя). Возвращённую функцию можно вызвать сразу же:

function chain_print(...)
  print(...)
  return chain_print
end
 
chain_print (1) ("alpha") (2) ("beta") (3) ("gamma")
--> 1
--> alpha
--> 2
--> beta
--> 3
--> gamma

В примере выше можно опустить скобки вокруг строковых литералов:

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 { }

Реализация


Теперь мы знаем почти всё, что нужно для того, чтобы наш декларативный код заработал. Напомню как он выглядит:

build_message_box = gui:dialog "Message Box"
{
  gui:label "Hello, world!" { font_size = 20 };
  gui:button "OK" { };
}

Что же там написано?


Приведу эквивалентную реализацию без декларативных «выкрутасов»:

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).

Благодаря особенностям Луа реализация получилась дешёвой и достаточно гибкой и мощной.

В реальной жизни всё, конечно, несколько сложнее. В зависимости от решаемой задачи нашему механизму могут потребоваться достаточно серьёзные доработки.

Например, если на нашем микро-языке будут писать пользователи, нам понадобится поместить выполняемый код в песочницу. Также, нужно будет серьёзно поработать над понятностью сообщений об ошибках.

Описанный механизм — не панацея, и применять его нужно с умом как и любой другой. Но, тем не менее, даже в таком простейшем виде, декларативный код может сильно повысить читаемость программы и облегчить жизнь программистам.

Полностью работающий пример можно посмотреть здесь.

Дополнительное чтение

Просмотров: 1308 | Добавил: i_elf | Рейтинг: 0.0/0 |
Всего комментариев: 0
Имя *:
Email *:
Код *:
Форма входа
Поиск по сайту
Google Scholar

Мои сайты
  • Создать сайт
  • Творческий учитель
  • Сайт ООАКМР
  • Школьный сайт
  • Информатика учебник (будет)
  • Математические основы информатики
  • РоЖдЕнИе ИдЕи
  • ВиРтУаЛьНыЙ мУзЕй
  • О тебе и обо мне

  • Copyright MyCorp © 2025
    Бесплатный хостинг uCoz