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

    Луа (Lua) — мощный, быстрый, лёгкий, расширяемый и встраиваемый скриптовый язык программирования. Луа удобно использовать для написания бизнес-логики приложений.

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

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

    Пример


    В качестве наивного примера возьмём код создания диалогового окна с текстовым сообщением и кнопкой в императивном стиле:

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

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

    Основы


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

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


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

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

    Таблицы


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

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

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

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

    = { }

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

    t[1] = "one"<br/>
    t["one"] = 1

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

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

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

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

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

    = { [1] = "one"[2] = "two"[3] = "three" }<br/>
    = { "one""two""three" }

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

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

    = { ["one"] = 1 }<br/>
    = { 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).

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

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

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

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

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

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

    Комментарии 18

      +10
      Отличная статья!
        +1
        Спасибо!
        +2
        Спасибо!
        Из документации можно добавить Справочное руководство по языку Lua 5.1

          0
          Рад, что понравилось! Ссылку добавил. Если из русскоязычных ресурсов есть ещё что-то стоящее, говорите, добавлю.
          0
          Статья действительно интересная. Очень понятно и доходчиво написано.
            0
            Спасибо!
            0
            В следующей статье можете описать привязки GUI libs к луа?
              0
              Могу. Интересует что-то конкретное?

              Просто существующих вариантов привязки различных GUI очень много (и идеального мне пока не попалось).

              Могу написать про какую-то одну библиотеку, либо в обзорном порядке по какой-нибудь операционной системе (или про кроссплатформенные варианты), либо вообще — как написать свои биндинги к чему-нибудь. :-)
                0
                мне например интересно что то типа fxruby, и желательно пару виз. редакторов под него.
                Просто мне луа понравился в декларативном плане, думаю перейти с руби на него
                  0
                  Fox Toolkit, значит? Есть старенький fxlua (не обновлялся с 2005-го года)…

                  Кстати, из графических библиотек наиболее развит сейчас, насколько я понимаю, lqt. Из мощных есть ещё, как минимум, wxlua и iup.

                  Но тут уже, и правда, статью писать надо. :-)
                    0
                    не не, qt ненадо. либо wx либо gtk.
              +1
              Давно хотел выучить Lua. Спасибо за статью!
                +3
                Рад помочь! :-)

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

                Здесь рассказан лишь кусочек из того, что нужно знать, чтобы грамотно писать на Луа вообще. И, в том, что рассказано, акценты смещены именно на декларативный подход.

                Это я к тому, что по данной статье Луа, увы, не выучить. :-) Рекомендую начать с Programming in Lua. Отличная книга, хотя и не переведена на русский.
                +2
                Внушает. Спасибо.

                Могу порекомендовать для кода «shopping_list = function(items)» прокомментировать почему странная конструкция со скобочками передается как аргумент :). Мне потребовалось некоторое время чтобы это понять, а LUA я знаю неплохо O_O. Запись столбиком выбивает из колеи ^_^.
                  0
                  Вам спасибо!

                  Да, пожалуй что, нужно явно расписать соответствующий вариант со скобками. Так и сделаю.

                  P.S. Прошу прощения за придирки, но очень режет взгляд. Lua лучше не писать заглавными буквами. Это не акроним.
                    0
                    Добавил вариант со скобками. Стало ли нагляднее? Может быть лучше написать ещё подробнее?..

                    Действительно, это ключевой момент в синтаксисе, который часто бывает не очевиден для тех, кто с ним раньше не сталкивался.
                  0
                  Добавил в конец статьи ссылку на «The evolution of an extension language: a history of Lua». Там хорошо написано про то, откуда в Луа декларативный синтаксис. (Искать в тексте «Sol».)
                  • НЛО прилетело и опубликовало эту надпись здесь

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

                    Самое читаемое