📜 ⬆️ ⬇️

Basics of declarative programming in Lua

Lua is a powerful, fast, easy, extensible and embedded scripting programming language. Luah is convenient to use for writing business logic applications.

Separate parts of application logic are often conveniently described in a declarative style . The declarative programming style differs from the much more imperative imperative in that it describes, first of all, what is something and not exactly 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 Lua's strengths is good support for declarative style. In this article, I will briefly describe the basic declarative means provided by the language of Lua.

Example


As a naive example, let's take the code for creating a dialog box with a text message and an imperative button:
')
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

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

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

Much more visible. But how to make it work?

The basics


To understand what's the matter, you need to know about some features of the language of the Loa. I will superficially talk about the most important for understanding this article. More information can be found at the links below.

Dynamic typing


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

a = "the meaning of life" --> , <br/>
a = 42 -->

Tables


Tables (table) - the main means of data composition in Lua. A table is a record and an array and a dictionary and set and object.

For programming on Lois it is very important to know this type of data well. I will briefly discuss only 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 the table t the string “one” by key 1 and the number 1 by key “one”:

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

The contents of the table can be specified when creating it:

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

A table in Lua can contain keys and values ​​of all types (except nil). But most often positive keys are used as keys (array) or strings (record / dictionary). 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 successive 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 issuing implicit ones.

The following two recording forms are equivalent:

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

Secondly: When using string literals as keys, quotation marks and square brackets can be omitted if the literal satisfies the constraints imposed on the identifiers .

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

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

Similarly, for indexing when writing ...

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

... And while reading:

print ( t [ "one" ] ) <br/>
print ( t.one )

Functions


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

Functions in Lua can be created dynamically anywhere in the code. At the same time, 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 ) <br/>
return function ( value ) <br/>
return value * coeff<br/>
end <br/>
end <br/>
<br/>
local x5 = make_multiplier ( 5 ) <br/>
print ( x5 ( 10 ) ) --> 50

It is important to remember that the “declaration of a function” in Lua is actually syntactic sugar that hides the creation of a value of the “function” type and assigns 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

A function call without parentheses


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

String literal:

my_name_is = function ( name ) <br/>
print ( "Use the force," , name ) <br/>
end <br/>
<br/>
my_name_is "Luke" --> Use the force, Luke

Sugarless:

my_name_is ( "Luke" )

Table Designer:

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

Sugarless:

shopping_list ( <br/>
{ <br/>
milk = 2 ; <br/>
bread = 1 ; <br/>
apples = 10 ; <br/>
} <br/>
)

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

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

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

For clarity, I give the equivalent code without the "frills":

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

Methods


Objects in Lua are most often implemented using tables.

The methods usually hide the function values ​​that are obtained by indexing the table by a string key identifier.

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

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

With a colon:

myobj = { a_ = 5 } <br/>
<br/>
function myobj:foo ( b ) <br/>
print ( self.a_ + b ) <br/>
end <br/>
<br/>
myobj:foo ( 37 ) --> 42

Without a colon:

myobj = { a_ = 5 } <br/>
<br/>
function myobj.foo ( self, b ) <br/>
print ( self.a_ + b ) <br/>
end <br/>
<br/>
myobj.foo ( myobj, 37 ) --> 42

Sugar free:

myobj = { [ "a_" ] = 5 } <br/>
<br/>
myobj [ "foo" ] = function ( self, b ) <br/>
print ( self [ "a_" ] + b ) <br/>
end <br/>
<br/>
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 in the case 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 variant with a colon, a temporary variable is needed:

do <br/>
local tmp = get_myobj ( ) <br/>
tmp.foo ( tmp, 37 ) <br/>
end

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

foo:bar "" <br/>
foo:baz { }

Implementation


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

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

What is written there?


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

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


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

We describe them in pseudocode:

 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


In the interface of the gui object, a pattern is clearly visible - a method that takes a portion of the arguments and returns a function that takes the remaining arguments and returns the final result.

For simplicity, we will assume that we build 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 the example code:

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

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

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

The situation with [gui_element] has 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 obviously very similar in structure to build_message_box (). The methods for generating the gui object will also be similar.

The following generalization suggests itself:

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

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

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 )

The implementation of the gui: label () and gui: button () methods became obvious:

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 )

What have we got?


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

As a result of our work, we, in fact, implemented using Lua's own domain-specific declarative language for describing the “toy” user interface (DSL) .

Due to the peculiarities of the Lua, the implementation was cheap and quite flexible and powerful.

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

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

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

A fully working example can be found here .

Additional reading

Source: https://habr.com/ru/post/77413/


All Articles