Lua declarative programming basics

    Lua (the Lua) - a powerful, fast, lightweight, extensible and embeddable scripting language. Lua is convenient for writing business application logic.

    Parts of the application logic are often conveniently described in a declarative style . The declarative style of programming differs from the more familiar one by many imperative ones in that it describes, first of all, what is something and not how it is created. Writing code in a declarative style often allows you to hide unnecessary implementation details.

    Lua is a multi - paradigm programming language . One of the strengths of Lua is a good support for the declarative style. In this article, I will briefly describe the basic declarative tools provided by the Lua language.

    Example


    As a naive example, let's take the code for creating a dialog box with a text message and a button in an imperative style:

    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

    In a declarative style, this code might look like this:

    build_message_box = gui:dialog "Message Box"

    {

      gui:label "Hello, world!" { font_size = 20 };

      gui:button "OK" { };

    }

    Much clearer. But how to make it work?

    The basics


    To understand what’s the matter, you need to know about some of the features of the Lua language. I will superficially talk about the most important for understanding this article. For more information, please follow the links below.

    Dynamic typing


    It is important to remember that Lua is a language with dynamic typification. This means that the type in the language is not associated with a variable, but with its value. The same variable can take on different types of values:

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

    a = 42                    --> стало число

    Tables


    Tables are the primary means of compiling data in Lua. The table is both record and array and dictionary and set and object.

    For programming in Lua it is very important to know this data type well. I will briefly dwell only on the most important details for understanding.

    Tables are created using the "table constructor" - a pair of curly braces.

    Create an empty table t:

    t = { }

    We write in table t the string “one” by key 1 and the number 1 by key “one”:

    t[1] = "one"

    t["one"] = 1

    The contents of the table can be specified during its creation:

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

    A table in Lua can contain keys and values ​​of all types (except nil). But most often positive integer numbers (array) or strings (record / dictionary) are used as keys. The language provides special tools for working with these types of keys. I will focus only on the syntax.

    First: when creating a table, you can omit the positive integer keys for consecutive elements. In this case, the elements receive the keys in the same order in which they are specified in the table constructor. The first implicit key is always one. Explicitly specified keys are ignored when implicitly issued.

    The following two forms of recording are equivalent:

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

    t = { "one", "two", "three" }

    Second: When using string literals as keys, you can omit quotation marks and square brackets if the literal satisfies the restrictions imposed on louid identifiers .

    When creating a table, the following two forms of writing are equivalent:

    t = { ["one"] = 1 }

    t = { one = 1 }

    Similarly for indexing when writing ...

    t["one"] = 1

    t.one = 1

    ... And while reading:

    print(t["one"])

    print(t.one)

    Functions


    Functions in Lua are first class values . This means that the function can be used in all cases, such as, for example, the string: assign to a variable, store in the table as a key or value, pass as an argument or return value to another function.

    Functions in Lua can be created dynamically anywhere in the code. Moreover, not only its arguments and global variables are available inside the function, but also local variables from external scopes. Functions in Lua, in fact, are closures .

    function make_multiplier(coeff)

      return function(value)

        return value * coeff

      end

    end

     

    local x5 = make_multiplier(5)

    print(x5(10)) --> 50

    It is important to remember that the “declaration of a function” in Lua is actually syntactic sugar, hiding the creation of a value of the type “function” and assigning it to a variable.

    The following two ways to create a function are equivalent. A new function is created and assigned to the global variable mul.

    With sugar:

    function mul(lhs, rhs) return lhs * rhs end

    Sugarless:

    mul = function(lhs, rhs) return lhs * rhs end

    Function call without parentheses


    In Lua, you do not have to put parentheses when calling a function with a single argument , if this argument is a string literal or a table constructor . This is very convenient when writing code in a declarative style.

    String literal:

    my_name_is = function(name)

      print("Use the force,", name)

    end

     

    my_name_is "Luke" --> Use the force, Luke

    Sugarless:

    my_name_is("Luke")

    Table constructor:

    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

    Sugarless:

    shopping_list(

          {

            milk = 2;

            bread = 1;

            apples = 10;

          }

      )

    Call chains


    As I mentioned, a function in Lua can return another function (or even itself). The returned function can be called immediately:

    function chain_print(...)

      print(...)

      return chain_print

    end

     

    chain_print (1) ("alpha") (2) ("beta") (3) ("gamma")

    --> 1

    --> alpha

    --> 2

    --> beta

    --> 3

    --> gamma

    In the example above, you can omit the parentheses around string literals:

    chain_print (1) "alpha" (2) "beta" (3) "gamma"

    For clarity, I’ll give an equivalent code without “tricks”:

    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

    Methods


    Objects in Lua - most often implemented using tables.

    Methods usually hide function values ​​obtained by indexing a table by a string identifier key.

    Lua provides a special syntactic sugar for declaring and calling methods - the colon. The colon hides the first argument to the method - self, the object itself.

    The following three forms of recording are equivalent. The global variable myobj is created, into which the object table is written with the single method foo.

    With a colon:

    myobj = { a_ = 5 }

     

    function myobj:foo(b)

      print(self.a_ + b)

    end

     

    myobj:foo(37) --> 42

    Without a colon:

    myobj = { a_ = 5 }

     

    function myobj.foo(self, b)

      print(self.a_ + b)

    end

     

    myobj.foo(myobj, 37) --> 42

    Absolutely Sugar Free:

    myobj = { ["a_"] = 5 }

     

    myobj["foo"] = function(self, b)

      print(self["a_"] + b)

    end

     

    myobj["foo"](myobj, 37) --> 42

    Note: As you can see, when calling a method without using a colon, myobj is mentioned twice. The following two examples are obviously not equivalent when get_myobj () is executed with side effects.

    With a colon:

    get_myobj():foo(37)

    Without a colon:

    get_myobj().foo(get_myobj(), 37)

    For the code to be equivalent to the colon option, a temporary variable is needed:

    do 

      local tmp = get_myobj()

      tmp.foo(tmp, 37) 

    end

    When calling methods through the colon, you can also omit parentheses if the method receives the only explicit argument - a string literal or a table constructor:

    foo:bar ""

    foo:baz { }

    Implementation


    Now we know almost everything that is needed for our declarative code to work. Let me remind you how it looks:

    build_message_box = gui:dialog "Message Box"

    {

      gui:label "Hello, world!" { font_size = 20 };

      gui:button "OK" { };

    }

    What is written there?


    I will give an equivalent implementation without declarative "tricks":

    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 object interface


    As we can see, all the work is done by the gui object, the “constructor” of our build_message_box () function. Now the outlines of its interface are already visible.

    We describe them in pseudo-code:

    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
    

    Declarative method


    The gui object interface clearly shows the template - a method that accepts part of the arguments and returns a function that takes the remaining arguments and returns the final result.

    For simplicity, we assume that we are building a declarative model on top of the existing gui_builder API, mentioned in the imperative example at the beginning of the article. Let me remind you the example code:

    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

    Let's try to imagine what the gui: dialog () method might look like:

    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

    The situation with [gui_element] cleared up. This is a constructor function that creates the corresponding dialog element.

    The build_message_box () function creates a dialog, calls constructor functions for child elements, and then adds these elements to the dialog. The constructor functions for the dialog elements are clearly very similar in structure to build_message_box (). The gui object methods that generate them will also be similar.

    This suggests at least the following generalization:

    function declarative_method(method)

      return function(self, name)

        return function(data)

          return method(self, name, data)

        end

      end

    end

    Now gui: dialog () can be written more clearly:

    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)

    The implementation of the gui: label () and gui: button () methods has become apparent:

    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)

    What did we get?


    The problem of improving the readability of our naive imperative example has been successfully resolved.

    As a result of our work, we, in fact, realized with the help of Luah our own subject-oriented declarative language for describing the "toy" user interface (DSL) .

    Due to the features of Lua, the implementation turned out to be cheap and quite flexible and powerful.

    In real life, everything, of course, is somewhat more complicated. Depending on the problem being solved, our mechanism may require quite serious improvements.

    For example, if users write in our micro-language, we will need to put the executable code in the sandbox . Also, you will need to seriously work on the comprehensibility of error messages.

    The described mechanism is not a panacea, and it must be applied wisely like any other. But, nevertheless, even in its simplest form, declarative code can greatly increase the readability of a program and make life easier for programmers.

    A fully working example can be found here .

    Additional reading


    Also popular now: