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.
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:
In a declarative style, this code might look like this:
Much clearer. But how to make it work?
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.
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:
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:
We write in table t the string “one” by key 1 and the number 1 by key “one”:
The contents of the table can be specified during its creation:
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:
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:
Similarly for indexing when writing ...
... And while reading:
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 .
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:
Sugarless:
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:
Sugarless:
Table constructor:
Sugarless:
As I mentioned, a function in Lua can return another function (or even itself). The returned function can be called immediately:
In the example above, you can omit the parentheses around string literals:
For clarity, I’ll give an equivalent code without “tricks”:
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:
Without a colon:
Absolutely Sugar Free:
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:
Without a colon:
For the code to be equivalent to the colon option, a temporary variable is needed:
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:
Now we know almost everything that is needed for our declarative code to work. Let me remind you how it looks:
I will give an equivalent implementation without declarative "tricks":
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:
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:
Let's try to imagine what the gui: dialog () method might look like:
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:
Now gui: dialog () can be written more clearly:
The implementation of the gui: label () and gui: button () methods has become apparent:
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 .
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
- Lua Programming Manual ( Translation )
- Programming in lua
- Lua Unofficial Frequently Asked Questions
- Lua programming gems
- Lua Users Wiki
- The evolution of an extension language: a history of Lua - A 2001 article in which, in particular, the sources of declarative syntax in Lua are clearly visible.