Not another programming language. Part 1: Domain Logic

  • Tutorial


Recently, a huge number of new programming languages ​​have appeared on the market: Go, Swift, Rust, Dart, Julia, Kotlin, Hack, Bosque - and this is only one of those that are heard.
The value of what these languages ​​bring to the world of programming is hard to overestimate, but as Y Combinator noted right last year when speaking about development tools:
Frameworks are getting better, languages ​​are a little smarter, but basically we do the same.
This article will talk about a language built on an approach that is fundamentally different from the approaches used in all existing languages, including those listed above. By and large, this language can be considered a general-purpose language, although some of its capabilities and the current implementation of the platform built on it, nevertheless, probably limit its application to a slightly narrower area - the development of information systems.

I’ll make a reservation right away, it’s not about an idea, a prototype, or even MVP, but about a full-fledged production-ready language with all the necessary infrastructure language - from the development environment (with a debugger) to the automatic support of several versions of the language (with automatic merge bugfixes between them , release-note, etc.). In addition, using this language, several dozen projects of complexity of the ERP level have already been implemented, with hundreds of simultaneous users, terabyte databases, “yesterday's deadlines”, limited budgets and developers without experience in IT. And all this at the same time. Well, of course, it should be noted that now is not the year 2000, and all these projects were implemented on top of existing systems (which wasn’t there), which means that at first it was necessary to do “as it was” gradually, without stopping the business, and then, also gradually make "as it should be." In general, this is how to sell the first electric cars not to wealthy hipsters in California, but to low-cost taxi services somewhere in Omsk.

A platform built in this language is released under the LGPL v3 license. Honestly, I didn’t want to write it right in the introduction, since this is far from the most important advantage of it, but, talking to people working in one of its main potential markets - ERP platforms, I noticed one feature: all these people without exception say that even if you do the same that is already on the market, but for free, then it will already be very cool. So leave it here.

Bit of theory


Let's start with the theory to highlight the difference in the fundamental approaches used in this and other modern languages.

A small disclaimer, further considerations to some extent are an attempt to pull an owl on a globe, but with a fundamental theory in programming, in principle, let's say bluntly, not really, so you have to use what you have.

One of the very first and main tasks solved by programming is the task of calculating the values ​​of functions. From the point of view of computational theory, there are two fundamentally different approaches to solving this problem.

The first such approach is various machines (the most famous of which is a Turing machine) - a model that consists of the current state (memory) and a machine (processor), which at each step changes this current state in one way or another. This approach is also called Von Neumann architecture, and it is he who underlies all modern computers and 99 percent of existing languages.

The second approach is based on the use of operators; it is used by so-called partially recursive functions(hereinafter CRF). Moreover, the most important difference of this approach is not in the use of operators as such (operators, for example, are also in structural programming using the first approach), but in the possibility of iterating over all values ​​of the function (see the operator of minimizing the argument) and in the absence of state in calculation process.

Like the Turing machine, partially recursive functions are Turing complete, that is, they can be used to specify any possible calculation. Here, we immediately clarify that both the Turing machine and the CRF are only minimal bases, and then we will talk about them just as approaches, that is, a model with a processor memory and a model with operators without using variables and the possibility of iteration over all values functions respectively.

The CRF as an approach has three main advantages:

  • It is much better optimized. This applies both directly to the optimization of the process of calculating the value, and the possibility of parallelism of such a calculation. In the first approach, the aftereffect, on the contrary, introduces a very great complexity into these processes.
  • It is much better incremented, that is, for a constructed function, it can be much more efficient to determine how its values ​​will change when the values ​​of the functions that this built function uses change. Strictly speaking, this advantage is a special case of the first, but it is precisely this that gives a huge number of possibilities, which basically cannot be in the first approach, therefore it is highlighted as a separate item.
  • It is much easier to understand. That is, roughly speaking, the description of the function of calculating the sum of one indicator in the context of two other indicators is much easier to understand than if the same is described in terms of the first approach. However, in algorithmically complex problems the situation is diametrically opposite, but it is worth noting that algorithmically complex problems in the vast majority of areas are good if 5%. In general, to summarize a little, the CRF is mathematics, and Turing machines are computer science. Accordingly, mathematics is studied almost in kindergarten, and computer science is optional and from high school. So-so comparison, of course, but still gives some kind of metric in this matter.

Turing machines have at least two advantages:

  • Already mentioned best applicability in algorithmically complex problems
  • All modern computers are built on this approach.

Plus, in this comparison we are talking only about data computation tasks; in problems of changing data, Turing machines can’t get along anyway.

Having read to this place, any attentive reader will ask a reasonable question: “If the CRF approach is so good, why is it not used in any common modern language?”. So, in fact, this is not so, it is used, moreover, in the language that is used in the vast majority of existing information systems. As you might guess, this language is SQL. Here, of course, the same attentive reader will reasonably object that SQL is the language of relational algebra (that is, working with tables, not functions), and it will be right. Formally. In fact, we can recall that the tables in the DBMS are usually in the third normal form, that is, they have key columns, which means that any remaining column of this table can be considered as a function of its key columns. Not obvious, frankly. And then why SQL has not grown from a relational algebra language into a full-fledged programming language (that is, working with functions) is a big question. In my opinion, there are many reasons for this, the most important of which is “a Russian (actually any) person cannot work on an empty stomach, but does not want to work on a well-fed one,” in the sense that, as practice shows, the work necessary for this it’s truly titanic and carries too many risks for small companies, and for large companies - firstly, everything is fine, and secondly, it is impossible to force this work with money - quality is more important than quantity. Actually, the most obvious illustration of what happens when people try to solve a problem the most important of which is “a Russian (in fact, any) person cannot work on an empty stomach, but doesn’t want a full one,” in the sense that, as practice shows, the work required for this is truly titanic and carries too many risks for small companies, and for large companies - firstly, everything is fine, and secondly, it’s impossible to force money to do this work - here quality is more important than quantity. Actually, the most obvious illustration of what happens when people try to solve a problem the most important of which is “a Russian (in fact, any) person cannot work on an empty stomach, but doesn’t want a full one,” in the sense that, as practice shows, the work required for this is truly titanic and carries too many risks for small companies, and for large companies - firstly, everything is fine, and secondly, it’s impossible to force money to do this work - here quality is more important than quantity. Actually, the most obvious illustration of what happens when people try to solve a problem it’s impossible to force money to do this — quality is more important than quantity. Actually, the most obvious illustration of what happens when people try to solve a problem it’s impossible to force money to do this — quality is more important than quantity. Actually, the most obvious illustration of what happens when people try to solve a problemthe quantity, not the quality , is Oracle, which even managed to implement the most basic application of incrementality - updated materialized representations - so that this mechanism has a number of restrictions that are several pages in size (in fairness, Microsoft is still worse ). However, this is a separate story, perhaps there will be a separate article about it.

At the same time, it is not that SQL is bad. Not. At its level of abstraction, it performs its functions perfectly, and the current platform implementation uses it a little less than fully (in any case, much more than all other platforms). Another thing is that immediately after its birth, SQL actually stopped in development and did not become what it could become, namely, the language that will be discussed now.

But enough theory, it's time to go directly to the language.

So, we meet :


Specifically, this article will be the first part of three (since there is still too much material, even for two articles), and it will only talk about the logical model - that is, only about what is directly related to the functionality of the system and has nothing to do with processes development and implementation (performance optimization). Moreover, we will talk only about one of the two parts of the logical model - the logic of the subject area. This logic determines what information the system stores and what you can do with this information (when developing business applications, it is also often called business logic).

Graphically, all concepts of domain logic in lsFusion can be represented by the following picture:


The arrows in this picture indicate the directions of use by the concepts of each other, thus, the concepts form a kind of stack, and, accordingly, it is in the order of this stack that I will talk about them.


Properties


A property is an abstraction that takes one or more objects as parameters and returns some object as a result. The property has no aftereffect, and, in fact, is a pure function, however, unlike the latter, it can not only calculate values, but also store them. Actually, the name “property” itself is borrowed from other modern programming languages, where it is used for approximately the same purposes, but it is nailed to encapsulation and, therefore, is supported only for functions with one parameter. Well, the fact that this very word “property” is shorter than “pure function”, plus has no unnecessary associations, played in favor of using this very term.

Properties are set recursively using a predefined set of operators. There are a lot of these operators, so we will consider only the main ones (these operators cover 95% of any average-static project).

Primary Property (DATA)


The primary property is a property whose value is stored in the database and can change as a result of the corresponding action (about it a little later). By default, the value of each such property for any set of parameters is equal to a special NULL value.
quantity = DATA INTEGER (Item);
isDayOff = DATA BOOLEAN (Country, DATE);
When using the primary property operator, you must specify which classes the created property accepts for input (about the classes themselves also a little later), and which class of value this property can return.

In fact, this operator generalizes fields and collections in modern languages. So:

class X { 	
    Y y; 	
    Map f; 	
    Map> m; 	
    List n;
    LinkedHashSet l; // упорядоченное множество 
    static Set s;
}

Equivalent to:
y = DATA Y (X);
f = DATA Z (X, Y);
m = DATA Z (X, Y, M);
n = DATA Y (X,INTEGER);
l = DATA INTEGER (X,Y);
s = DATA BOOLEAN (Y);

Composition (JOIN), Constant, Arithmetic (+, -, /, *), Logical (AND, OR), String (+, CONCAT), Comparison (>, <, =), Choice (CASE, IF), Belonging to the class (IS)

f(a) = IF g(h(a)) > 5 AND a IS X THEN ‘AB’ + ‘CD’ ELSE x(5);
Everything is more or less standard here, so stopping at these operators in detail does not make much sense. The only thing that is probably worth noting:

  • In logical operators and selection operators, as conditions, one can use not only properties with values ​​of logical types, but any properties in general. Accordingly, the condition in this case is the certainty of the property value (that is, the difference from NULL). Actually, the logical type in lsFusion itself is, in fact, a constant, that is, its set of values ​​consists of exactly one element - the TRUE value (the FALSE role is NULL), no roof-bearing 3-states.
  • For arithmetic and string operators, there are special forms of working with NULL: (+), (-), CONCAT with a separator. When using these forms:
    • in arithmetic operators: NULL at the input is interpreted as 0, and at the output, on the contrary, 0 is replaced by NULL (i.e. 5 (+) NULL = 5, 5 (-) 5 = NULL, but 5 + NULL = NULL and 5 - 5 = 0).
    • in string operators: NULL at the input is ignored and accordingly the separator is not added (i.e. CONCAT '', 'John', 'Smith' = 'John Smith', and CONCAT '', 'John', NULL = 'John', but ' John '+' '+ NULL = NULL).
  • For the simple choice operator (IF), there is (and is very often used) a postfix form: f (a) IF g (a), which returns f (a) if g (a) is not NULL, and NULL otherwise.

Grouping (GROUP)


Grouping is the most commonly used set operator. This operator takes a property and for all its values ​​calculates some aggregate function (for example, the sum) in the context of the values ​​of other properties.

In terms of syntax, there are two forms of this operator:

  • Functional:
    sum(Invoice i) = GROUP SUM sum(InvoiceDetail id) IF invoice(id) = i;
    currentBalance(Sku sk) = GROUP SUM currentBalance(sk, Stock st);
    This form allows closure to the lexical context, that is, inside the operator you can use the parameters of the external context (in the examples above, the parameters i and sk). The peculiarity of the functional form is that it can be used in expressions, that is, to write something like:
    x() = (GROUP SUM f(a)) + 5;
  • SQL style:
    sum = GROUP SUM sum(InvoiceDetail id) BY invoice(id);
    currentBalance = GROUP SUM currentBalance(Sku sk, Stock st) BY sk;
    Unlike the functional, this form of the operator can be used only when declaring properties (like, say, the operator of creating a primary property)

From the point of view of conciseness of the code, it makes sense to use the first form when the grouping is by parameters (example with the remainder), the second - by properties (example with an invoice). Although, by and large, this is still a matter of taste, to whom it is more familiar (for people who worked more with functional programming, the first form would be more familiar, for those working with SQL - the second). By the way, if you wish, you can use a mixture of these forms (that is, when you can access the upper parameters and use the BY option), something like:
// BY отображается только на неиспользованные параметры, то есть s
sum(DATE from, Stock s, DATE to) = GROUP sum(Invoice i) IF date(i) >= from AND date(i) <=to BY stock(i); 
but, to be honest, I would not recommend doing this, since such a mapping, in my opinion, is too implicit.

As an aggregating function, in addition to the amount, the following are also supported:

  • High / low
  • String concatenation in the given order
  • The last value in the given order.

Partitioning / Organizing (PARTITION ... ORDER)


The grouping operator described above splits all objects (or rather, sets of objects) in the system into groups, after which it calculates a certain value for each group. However, in some cases, the value needs to be calculated not for the group itself, but for directly grouped sets of objects (but to do this in the context of the group into which this set belongs). To perform this kind of computation, a special splitting / ordering operator exists in the language.
place(Team t) = PARTITION SUM 1 ORDER DESC points(t) BY conference(t);
Note that, generally speaking, splitting can be performed without ordering, and ordering without splitting, but nevertheless, in the vast majority of cases, these operations are performed together, so they are combined into one operator.

The analogue of this operator in SQL (and the means by which it is implemented) are window functions (OVER PARTITION BY ... ORDER BY).

Recursion (RECURSION)


Recursion is probably the most complex operator for working with sets. It is needed to implement calculations with an unknown number of iterations in advance, in particular, to work with graphs.

For the recursion operator, you must specify the initial property and the step property. Accordingly, the algorithm for calculating this operator is as follows (hereinafter an almost verbatim quote from the documentation):

  • First, an intermediate property (result) is constructed recursively with an additional first parameter (operation number) as follows:
    • result (0, o1, o2, ..., oN) = initial (o1, ..., oN), where initial is the initial property
    • result (i + 1, o1, o2, ..., oN) = step (o1, ..., oN, $ o1, $ o2, ..., $ oN) IF result (i, $ o1, $ o2 , ..., $ oN), where step is the property of the step.
  • Then, for all values ​​of the obtained property, the sum is calculated in the context of all its parameters, with the exception of the operation number (that is, o1, o2, ..., oN). Theoretically, instead of a sum, there can be any aggregating function, but in the current implementation only the sum is supported.

Not the most obvious definition, frankly, therefore, the essence of this operator is probably easier to understand from the examples:
// итерация по integer от from до to (это свойство по умолчанию входит в модуль System)
iterate(i, from, to) = RECURSION i=from STEP i=$i+1 AND i<=to CYCLES IMPOSSIBLE;
 
// считает количество различных путей от a до b в графе (то есть, в частности, определяет достижимость)
edge = DATA BOOLEAN (Node, Node);
pathes 'Кол-во путей' (a, b) = RECURSION 1 IF b=a STEP 1 IF edge(b, $b);
 
// определяет, на каком уровне находится child от parent, и null, если не является потомком (тем самым это свойство можно использовать для определения всех child'ов)
parent  = DATA Group (Group);
level 'Уровень' (Group child, Group parent) = RECURSION 1 AND child IS Group AND parent = child STEP 1 IF parent = parent($parent);
 
// числа Фибоначчи, свойство высчитывает все числа Фибоначи до значения to, (после будет возвращать NULL)
fib(i, to) = RECURSION 1 IF (i=0 OR i=1) STEP 1 IF (i=$i+1 OR i=$i+2) AND i
Note that if splitting / ordering can be implemented using, say, grouping and composition, then the tasks of this operator can not be solved using other operators in principle.

By the way, it is funny that although the definition of this operator is very similar to the definition of the primitive recursion operator in the ChRF, in the ChRF primitive recursion can be applied only if the number of iterations is known in advance, and vice versa in lsFusion.

Recursive CTEs are an analogue of the recursion operator in SQL, however, the platform rarely uses them when executed, since there are a very large number of restrictions. In particular, in Postgres it is impossible to use GROUP BY for a step, which, in essence, means that when you run through the graph for vertices you cannot use marks, which means that the number of iterations grows exponentially. Therefore, in practice, the platform, as a rule, uses table functions with WHILE inside.

This concludes the description of property creation operators. These are not all operators, but the rest are either much less commonly used or belong to other levels of language abstraction and will be considered there.

Actions


An action is an abstraction that takes some objects as parameters and, using them in one way or another, changes the state of the system (both the one in which this action is performed and the state of any other external system). Here, of course, one could probably use the term “procedure”, but, firstly, it has already become obsolete quite a while, and secondly, the word itself is more cumbersome and incomprehensible than “action”.

In general, properties and actions are a kind of Yin and Yang programming in lsFusion. Properties use the HRF approach, actions use the Turing machine approach. Properties are processed on the database server, actions are processed on the application server (there really is a lot of magic here when the platform moves these processes between servers, so it’s more about where these abstractions are processed by default). Properties are responsible for storing and computing data; actions are responsible for changing. Etc.

It is worth noting that the division into properties and actions is implicit in other languages. So arithmetic / logical operators, variables, fields, and in general everything that can be used in expressions can be attributed to the logic of properties, everything else to the logic of actions. But if in other languages ​​this ratio is good if 3 by 97, then in lsFusion in an average project it is at least 60 by 40.

Actions, like properties, are set recursively using a predefined set of operators. These operators, again, are quite numerous (in fact, they are several times larger than the operators for creating properties), therefore, we also consider only the main ones.

Let's start with the operators responsible for the execution order:

Loop (FOR), Recursive Loop (WHILE)


Despite the same name, the loop in lsFusion differs significantly from the same concept in other programming languages, and is built on the iteration operation mentioned above for all sets of objects for which the value of the specified property is not NULL (we will call this property a loop condition).
FOR selected(Team team) DO
    MESSAGE 'Team ' + name(team) + ' was selected';
By default, iteration is in a non-deterministic order, but if necessary, this order can be specified explicitly:
showAllDetails(Invoice i) {
    FOR invoice(InvoiceDetail id) = i ORDER index(id) DO
        MESSAGE 'Sku : ' + nameSku(id) + ', ' + quantity(id);
}
Note that when creating a loop, its condition must introduce a new parameter, otherwise the platform will throw an error and suggest using the branch operator (IF).

A recursive loop (WHILE) differs from a regular loop only in that:

  • continues execution until there is at least one non-NULL value for the loop condition (in this sense, it is very similar to the recursion operator in properties)
  • not required to enter a new parameter

Call (EXEC), Sequence ({...}), Branching (CASE, IF), Interrupt (BREAK), Exit (RETURN)

f(a) {
    FOR t=x(b,a) DO {
        do(b);
        IF t>5 THEN
            BREAK;
    }
    MESSAGE 'Succeeded';
}
These operators are more or less standard and do not differ much from similar operators in other programming languages. It is clear that there are small nuances in the syntax, but compared to the differences in other operators, it makes no sense to dwell on them in detail.

Property Change (CHANGE)


This operator allows you to change the values ​​of primary properties. At the same time, he can do this, not only for one set of values ​​of objects, but also for all sets of objects for which the value of the specified property is not NULL. For instance:
// изменить скидку для выбранных товаров для клиента
setDiscount(Customer c)  {
    discount(c, Item i) <- 15 WHERE selected(i);
}
Note that the action described above is equivalent to:
setDiscount(Customer c)  {
    FOR selected(Item i) DO
        discount(c, i) <- 15;
}
And in fact, the platform, if it sees that there are no recursive dependencies in the loop body (that is, when the readable properties depend on the mutable ones, as in this case, assuming, for example, that selected and discount are the primary properties), then the platform itself automatically converts the second option to the first and performs one request. However, such optimization is a separate topic, which will be discussed in more detail in the following articles.

Adding Objects (NEW)


This operator adds an object of a given class (about classes now very soon, although there is nothing special, in any case, in their way of specifying, no). As well as for a property change operator, you can add not one, but many objects at once for a given condition.

The syntax of the add object operator is similar to the syntax of the property change operator:
newSku ()  {
    LOCAL addedSkus = Sku (INTEGER);
    NEW Sku WHERE iterate(i, 1, 3) TO addedSkus(i);
    FOR Sku s = addedSkus(i) DO {
        id(s) <- 425;
        name(s) <- 'New Sku : ' + i;
    }
}
However, obviously this syntax is usually not used, there is a special syntactic sugar for adding objects - the NEW option in the loop operator (FOR), which immediately introduces a new parameter for the added object (which is much more convenient):
FOR iterate(i, 1, 3) NEW s=Sku DO  {
    id(s) <- i;
    name(s) <- 'New Sku : ' + i;
}
If you need to add exactly one object, FOR can be omitted:
NEW s=Sku DO {
    id(s) <- 425;
    name(s) <- 'New Sku';
}
Adding an object from a physical point of view is nothing more than generating a unique identifier, and if there are several objects, the platform can generate these identifiers in one request for all objects at once.

Deleting Objects (DELETE)


Here, everything is quite simple and in many ways similar to the two upper operators - the delete operator removes one or many objects for a given condition:
DELETE Sku s WHERE name(s) = 'MySku';
As for changing a property, the “magic” of transferring the loop condition to the delete condition works for the delete operator.

Before moving on to the next operators, it is necessary to talk about another important concept used in the logic of actions.

Change Sessions


As mentioned earlier, an action as a result of its execution can change the state of the system in which it is executed. It is not always desirable to record these changes directly to the database, both from the point of view of integrity and from the point of view of ergonomics of the system. Therefore, the platform has the ability to accumulate these changes locally in the so-called change sessions.

Changes in a session can be changes to primary properties, as well as changes to object classes. The former are implemented using the property change operator described above, and the latter are implemented using the add / delete operators of objects.

Each time an action is performed, depending on the execution context, the current session is determined for it. For example, if an action is called as a handler for some event of the form (the most common case), then the session of this form will be the current session for it.

If an action refers to some property during the execution, then its value is calculated taking into account the changes made in the current session of this action. For example:
LOCAL f = INTEGER (INTEGER, INTEGER);

f(1,3) <- 6;
f(2,2) <- 4;
f(f(1,3),4) <- 5;
f(a,a) <- NULL; // удаляет все изменения с одинаковым 1-м и 2-м параметрами (то есть 2,2)

MESSAGE GROUP CONCAT a + ',' + b + '->' + f(a, b),' ; '; // выдаст 1,3->6 ; 6,4->5
Two basic operations are supported for a session: application (APPLY) and cancellation (CANCEL). It is important here not to confuse sessions with transactions; these are, strictly speaking, perpendicular concepts. So, sessions can exist for a long time, accumulating changes in temporary tables or an application server, and start a transaction only when applying session changes to a common database. How integrity is maintained is a separate topic, but in short, immediately after the start of the transaction, a check is made for possible changes in the value classes of all existing changes, respectively, incorrect changes are deleted. Canceling a session is simply clearing it of all changes accumulated in it.

Creating Sessions (NEWSESSION, NESTEDSESSION)


Sessions are created automatically at the highest operations on the stack (for example, calling an action from the navigator, through an http request, etc.). However, in the process of performing one action, it is often necessary to perform another action in a new session other than the current one. Usually, such a need arises if the context of the execution of the action is unknown, and by applying the changes to the current session “blindly”, one can accidentally apply “alien” changes (that is, those that were not to be applied). To implement this feature, the platform has a special NEWSESSION operator, when wrapped in which the action will be performed in a new session (in this case, at the end of this action, the session will automatically close). For instance:
run() {
    f(1) <- 2;
    APPLY;
    f(1) <- 1;
    NEWSESSION {
        MESSAGE f(1); // выдаст 2, то есть без учета изменений верхней сессии
        f(2) <- 5;
        APPLY;          
    }
    MESSAGE f(1); // выдаст 1, то есть изменение на 1 как было, так и осталось
}
True, when using new sessions, the question arises of how to transfer data between the current and the created sessions, if it is nevertheless necessary. So, if parameters are passed along the stack automatically:
run(Store s) {
    NEWSESSION
        MESSAGE 'I see that param, its name is: ' + name(s);
}
then changes to properties, by default, are not transmitted anywhere (see the example above). To solve this problem, the platform has a special NESTED option, which allows you to copy changes to the set properties into it when creating a session, and, conversely, when closing a session, copy changes to these properties back to the current session. This option is supported both directly in session creation statements and globally for a property (in this case it works as if this property would be explicitly specified as NESTED in each session creation statement). For instance:
g = DATA LOCAL NESTED INTEGER ();
run() {
    f(1) <- 1; g() <- 5;
    NEWSESSION NESTED (f) {
        MESSAGE f(1) + ' ' + g(); // выдаст 1 5
        f(1) <- 5; g() <- 7;
    }
    MESSAGE f(1) + ' ' + g(); // выдаст 5 7
}
The platform also supports the creation of so-called nested sessions. For a nested session:

  • all changes of the current session are automatically copied to the created session, that is, roughly speaking, a nested session <- the current session
  • when canceling changes in a nested session, it is not cleared, but returns to the state at the time of creation: nested session <- current session
  • when applying changes in a nested session, all its changes are copied back to the current session: current session <- nested session.

The mechanism of nested sessions is very convenient when you need to organize the input of a large amount of information (possibly consisting of several stages), but you must either apply all the changes at the end at the same time, or not apply anything at all. So, for example, if you need to enter some kind of large document, during the input of which, in turn, you must be able to enter the product if it is not there, but in order to:

  • the user could cancel the entry of this product and continue entering the document
  • if the user cancels the entry of the entire document, the entry of this product must also be canceled

Apply Changes (APPLY), Cancel Changes (CANCEL)


Application and cancellation of changes are operations for which sessions were actually created. In the description of the sessions, they have already been mentioned, and their semantics follows from their name. One thing worth noting:

  • When applying and reverting changes, all changes to the local primary properties are deleted. Sometimes this behavior is undesirable, therefore, as for creating sessions, the NESTED option is supported for these statements (with similar behavior).
  • При применении изменений есть возможность указать дополнительное действие, которое будет выполнено сразу после начала транзакции. Главное отличие выполнения этого дополнительного действия внутри транзакции от его выполнения сразу перед применением изменения заключается в том, что если применение по какой-либо причине будет отменено, то и изменения, сделанные в этом дополнительном действии, также будут отменены. Более того, если причиной отмены применения был конфликт записи (update conflict), а значит, применение будет автоматически выполнено еще раз, то в этом случае указанное дополнительное действие также будет выполнено еще раз. К примеру, такое поведение можно использовать для реализации долгосрочной пессимистичной блокировки:

// -------------------------- Object locks ---------------------------- //
 
locked = DATA User (Object);
lockResult = DATA LOCAL NESTED BOOLEAN ();
 
lock(Object object)  {
    NEWSESSION { 
        lockResult() < - NULL;
        APPLY SERIALIZABLE {
            IF locked(object) THEN {
                CANCEL;
            } ELSE {
                locked(object) <- currentUser();
                lockResult() <- TRUE;
            }
        }
    }
}
 
unlock(Object object)  {
    NEWSESSION
        APPLY locked(object) <- NULL;
}
PS: the above properties and actions are already declared in the Authentication system module, therefore, if necessary, it is possible (and recommended) to use them (here they are given only as an example). Although, generally speaking, pessimistic blocking in lsFusion is in principle not recommended, since the platform itself automatically perfectly resolves the vast majority of competitive access situations (for example, editing one document at the same time).

The next set of operators are the operators of creating properties, not actions, but they are by their nature closer to the logic of changes, not calculations, therefore they are described here (and not in properties).

Change Operators (PREV, CHANGED, SET, DROPPED)


For the session, a set of operators for working with changes is supported: obtaining the previous value in the session (PREV), determining whether the property value in the session (CHANGED) has changed, whether it has changed from NULL to a non-NULL value (SET), etc. In general, these operators are mainly used in the logic of events (about them a little later), but if necessary, they can be used inside actions called from anywhere, for example:
f = DATA INTEGER (INTEGER);
run() {
    f(1) <- 2;
    APPLY;
 
    f(1) <- 5;
    f(2) <- 3;
    MESSAGE GROUP SUM 1 IF CHANGED(f(a)); // определяет, для скольких значений f были изменения в этой сессии, выдаст 2
    MESSAGE 'Тек. значение: ' + f(1) + ', Пред. значение: ' + PREV(f(1)); // выдаст Тек. значение: 5, Пред. значение: 2
}
This concludes the action creation statements. Not because they ended, just like with properties, the rest are either very rarely used, or are closely related to the concepts of other levels of language abstraction and will be considered there.

Events


Actions answer the question “What to do?”, But do not answer the question “When to do this?”. To determine the moments when you need to perform certain actions, events exist in the platform.

I’ll make a reservation right away, we will go on about the events of the subject area, in addition to them, there are also form events in the presentation logic. These are two completely unrelated mechanisms, and we will dwell on the events of the form in an article on presentation logic. But in the future, events without specifying their type will be considered events of the subject area.

Domain events are of two types:

  • Synchronous - occur immediately after data change.
  • Asynchronous - occur at arbitrary points in time as the server manages to complete all specified processing and / or after a certain period of time.

In turn, from the point of view of the scope of changes, events can be divided into:

  • Local - occur locally for each session of changes.
  • Global - occur globally for the entire database.

Thus, events can be synchronous local, synchronous global, asynchronous local, and asynchronous global.

Advantages of synchronous events:

  • If necessary, you can cancel changes in the process if, for example, these changes do not satisfy the necessary conditions.
  • They guarantee greater integrity, since after the end of the recording of changes, the user is guaranteed to work with the updated data.

Benefits of asynchronous events:

  • You can immediately release the user, and perform the processing "in the background." This improves the ergonomics of the system, however, it is possible only when updating the data is not critical for the user's further work (for global events, for example, within the next 5-10 minutes, until the server has time to complete the next processing cycle).
  • Processing is grouped for a large number of changes, including those made by various users (in the case of global events), and, accordingly, are performed fewer times, thereby improving the overall system performance.

Benefits of local events:

  • The user sees the results of event processing immediately, and not just after he has saved them to a common database.

Benefits of global events:

  • They provide better performance and integrity, both due to the fact that processing is performed only after the changes are saved to a common database (that is, significantly less often), and due to the use of numerous DBMS capabilities related to working with transactions.

So far, only synchronous global and asynchronous local ones are supported in the platform (as the most commonly used, support for other types of events is also planned in the future), so we will simply talk about global and local events in the future.
ON { // по умолчанию глобальное, то есть будет выполняться при каждом APPLY
    MESSAGE 'Something changed';
}
However, as described above, in practice it’s better not to do it, since the message 'Something changed' will be issued when any (!) Changes are applied (regardless of what has changed in this session). As a rule, in events it is necessary to check that something specific has changed, and here the operators of work with changes come to the rescue (CHANGED, SET, DROPPED, etc.). Moreover, in practice, most events come down to a simple causal relationship, when something has changed, you need to do something. To implement this scenario, the platform has a special type of event - simple events:
// отправить email, когда остаток в результате применения изменений сессии стал меньше нуля
WHEN SET(balance(Sku s, Stock st) < 0) DO
      EMAIL SUBJECT 'Остаток стал отрицательным по товару ' + name(s) + ' на складе ' + name(st);

WHEN LOCAL CHANGED(customer(Order o)) AND name(customer(o)) == 'Best customer' DO
    discount(OrderDetail d) <- 50 WHERE order(d) = o;

In fact, simple events are nothing more than syntactic sugar. So, the first event is equivalent to:
ON {
    FOR SET(balance(Sku s, Stock st) < 0) DO
        EMAIL SUBJECT 'Остаток стал отрицательным по товару ' + name(s) + ' на складе ' + name(st);
}
But since using simple events, shooting yourself in the leg is much more difficult, and it is easier for the developer to write / read them, by default it is recommended to use simple events, and use ordinary events only to optimize the execution of really difficult cases.

Triggers are some analogue of simple events in SQL (or rather its extensions). However, triggers are limited to one table, they work entirely for writing, they are performed for each table separately (that is, they cannot be executed by one query), and there are many other things, and they are not used by the platform for implementing simple events.

It is important to note that inside the processing of events of the subject area, the behavior of some operators changes:

  • Discard changes - cancels the application of changes, and does not clear the session (this operator can only be used inside synchronous events)
  • Operators of work with changes - return the value at the time the processing of the previous event is completed, and not the current value in the database. However, for global events, these values ​​coincide, plus using a special option you can “return” these operators to standard mode and return the current value in the database.

Limitations


Platform restrictions determine which values ​​may have primary properties and which may not. In general, a constraint is defined as a property whose value should always be NULL:
// остаток не меньше 0
CONSTRAINT balance(Sku s, Stock st) < 0 
    MESSAGE 'Остаток не может быть отрицательным';

// "эмуляция" политики безопасности
CONSTRAINT DROPCHANGED(barcode(Sku s)) AND name(currentUser()) != 'admin' 
    MESSAGE 'Изменять штрих-код для уже созданного товара разрешено только администратору';

// в заказе можно выбирать только товары, доступные данному покупателю
CONSTRAINT sku(OrderDetail d) AND NOT in(sku(d), customer(order(d)))
    MESSAGE 'В заказе выбран недоступный пользователю товар для выбранного покупателя';
In fact, a constraint is a simple event in which the condition is a change to the non-NULL (SET) value of the property being limited, but processing is to show all its non-NULL values ​​and discard the changes made. That is, a restriction is nothing more than syntactic sugar, but just like simple events, restrictions are easier to read / write, so it is recommended to use them if possible.

As well as for events, there is a special subspecies for restrictions - simple restrictions (syntactic sugar for the most common cases of restrictions), but as practice has shown, in addition to limiting that a given property must be set (everything is more or less obvious here), simple restrictions are used very rarely, so we will not dwell on them in detail.

Classes


Well, here we come to the classes. Usually it is customary to start with them, but, strictly speaking, logically, classes are no more than one of the types of restrictions. For example:
f = DATA A (INTEGER);
means that if f has a non-NULL value, then this value must be of class A. That is, the upper example is equivalent:
f = Object (INTEGER);
CONSTRAINT f(i) AND NOT f(i) IS A MESSAGE 'Неправильный класс'; // f(i) => f(i) IS A
At the same time, if logically classes are at the top level of the stack, then physically - everything is exactly the opposite. Classes are not syntactic sugar (that is, they are implemented not through restrictions, as in the example above, but "natively"), respectively, working with them is very well optimized, which means that the general principle is as follows: if a problem can be solved using classes , it is better to solve it with the help of classes.

In general, the concept of classes in lsFusion is not very different from that in OOP. True, unlike OOP, lsFusion has no encapsulation. Anyway, bye. But even if encapsulation in lsFusion appears, it is only in the form of syntactic sugar, something like:
CLASS A {
    f = DATA LONG (INTEGER); // эквивалентно f = DATA LONG (A, INTEGER)
}
As in OOP, lsFusion supports class inheritance, including multiple:
CLASS Animal;
CLASS Transport;
CLASS Car : Transport;
CLASS Horse : Transport, Animal;
Inheritance in itself is not very useful, its main purpose is to use polymorphism in the mechanisms.

Polymorphism


In the current version of lsFusion, polymorphism is explicit. To implement it, an abstract property or action is first declared for some, possibly abstract, class:
speed = ABSTRACT LONG (Transport);
Then, when a specific class appears, it is possible / necessary to set the implementation of the declared abstract property, for example:
CLASS Breed;
speed = DATA LONG (Breed)
breed = DATA Breed (Animal);

speed(Horse h) += speed(breed(h)); // для лошади скорость берем из ее породы
Polymorphism is also supported for several parameters (the so-called multiple polymorphism):
CLASS Thing;
CLASS Ship : Thing;
CLASS Asteroid : Thing;

collide ABSTRACT (Thing, Thing);
collide(Ship s1, Ship s2) +{
    MESSAGE 'Ship : ' + name(s1) + ', Ship : ' + name(s2);
}
collide(Ship s1, Asteroid a2) +{
    MESSAGE 'Ship : ' + name(s1) + ', Asteroid : ' + name(a2);
}
collide(Asteroid a1, Ship s2) +{
    MESSAGE 'Asteroid : ' + name(a1) + ', Ship : ' + name(s2);
}
collide(Asteroid a1, Asteroid a2) +{
    MESSAGE 'Asteroid : ' + name(a1) + ', Asteroid : ' + name(a2);
}
Polymorphism, strictly speaking, refers to a physical model (development process), and not a logical one. So the server immediately after parsing turns the ABSTRACT statement into a selection statement:
speed(Transport t) = CASE 
    WHEN t IS Horse THEN speed(breed(t))
    // другие реализации
END
but as already mentioned without polymorphism, inheritance makes little sense, so we ran a little ahead.

In the future, it is planned that, in addition to explicit polymorphism, the language will also support implicit polymorphism, that is:
speed(Horse h) = speed(breed(h));
will simultaneously create a property for the horse, and add implementations to all abstract properties with the same name, which are suitable for classes (as is done in most modern languages). Moreover, for this the platform already has all the necessary infrastructure, but for various reasons they decided not to include this functionality in the first public version of the platform.

Inline classes


Above we talked only about custom classes, that is, classes that developers create. At the same time, the platform also supports the so-called built-in (primitive) classes: numbers, strings, dates, and so on. There is nothing much special in comparison with other languages, they must, however, be taken into account that in the current implementation they cannot be mixed with each other or with user classes. That is, a property cannot return a non-NULL value at the same time for a certain number or an object, that is, you cannot do this:
f = DATA LONG (LONG);
g = DATA LONG (A);
h(a) = OVERRIDE f(a), g(a); // платформа выдаст ошибку

Static Objects


Static (or built-in) objects - objects that are created at server startup and which cannot be deleted. In addition, static objects can be accessed as constants directly in the language:
CLASS Direction 'Направление' {
    left 'Налево',
    right 'Направо',
    forward 'Прямо'
}

result(dir) = CASE
    WHEN dir = Direction.left THEN 'Коня потеряешь'
    WHEN dir = Direction.right THEN 'Жизнь потеряешь'
    WHEN dir = Direction.forward THEN 'Голову потеряешь'
END
Otherwise, static objects are no different from other objects created by the user.

The analogs of static objects in modern programming languages ​​are enum'y, respectively, usually static objects are used exactly for the same purpose.

Aggregations


The class mechanism (both in lsFusion and in other languages) has at least three limitations:

  • Belonging to a class cannot be calculated (only set explicitly when adding and changing the class of an object).
  • A class is defined for only one object (and not for a set of objects).
  • It is not possible to inherit the same class several times.

To circumvent these restrictions, the platform has a mechanism of so-called aggregations.

Aggregation refers to the creation of a unique (aggregated) object corresponding to each non-NULL value of some aggregated property. For such an object, it is assumed that there are properties that map this object to each of the parameters of the aggregated property, and a property that, on the contrary, maps the parameters of the aggregated property to this object.

For instance:
// для каждого A создается объект класса B
b(A a) = AGGR B WHERE a IS A; 
// также неявно создается свойство a с одним параметром класса B и значением класса A, при этом b(a(b)) = b

createC = DATA BOOLEAN (A, B)
// для каждой пары A и B для которой задано createC создается объект класса C
// слева параметры можно не указывать, как и в любых других объявлениях свойств, они автоматически определяются из правой части
c = AGGR C WHERE createC(A a, B b); 
// также неявно создаются свойства a и b с одним параметром класса C и значениями классов А и B соответственно
Now let's take a more life-saving example, and show how aggregation can be used together with inheritance and polymorphism (which in practice is done in the vast majority of cases):
CLASS Shipment 'Поставка';
date = ABSTRACT DATE (Shipment);
CLASS Invoice 'Инвойс';
createShipment 'Создавать поставку' = DATA BOOLEAN (Invoice);
date 'Дата накладной' = DATA DATE (Invoice);
CLASS ShipmentInvoice 'Поставка по инвойсу' : Shipment;
// создаем поставку по инвойсу, если для инвойса задана опция создавать поставку
shipment(Invoice invoice) = AGGR ShipmentInvoce WHERE createShipment(invoice);
date(ShipmentInvoice si) += sum(date(invoice(si)),1); // дата поставки = дата инвойса + 1
In general, these three mechanisms (aggregation, inheritance, and polymorphism), as well as events and extensions (about extensions later in the article on the physical model) allow achieving modularity, if not ideal, then very close to it. For example, now ERP consists of approximately 1100 modules. Accordingly, from them you can select any subset of modules and assemble from this subset a solution in which there will be exactly what the customer needs. So, for some customers we have only non-food retail (about 50 modules), some only have production and wholesale, and some have almost all 1,100 plus another 300 of their own.

We’ll end with the logic of the domain, this is certainly not all, but even so, perhaps too much for one article. However, soon there will be at least two more articles describing the features of the language, one about the presentation logic, the second about the physical model, and there, unfortunately or fortunately, it will be difficult to manage with the phrases “everything is more or less standard here”, so as they say, do not go far from your screens.

Conclusion


Of course, in contrasting lsFusion with general-purpose languages, there is a certain share of cunning in the introduction. Yes, classes, aggregations, restrictions, events and other abstractions of the language, by and large, really do not belong to any specific subject area, and in one form or another exist, including in system programming (that is, for example, in the development conditional OS or DBMS). But implementing a virtual machine that supports the entire lsFusion specification (even without ACID), which will not be as heavy as modern SQL servers, will be very difficult. As a result, getting rid of the DSL lsFusion label is unlikely to succeed, which means that it is hardly necessary to rely on the favor of most system programmers - the main consumers of general-purpose languages. Strictly speaking, and SQL most of them do not like, there is too much magic under the hood, and in lsFusion this magic is even more. Of course, we will try to maximize this effect - a free license, github sources (both the platform itself and its entire infrastructure), the maximum use of existing ecosystems (IDE, reporting, VCS, automatic assemblies), slack and telegram channels communication, the presence in public repositories (linux and maven, again with the source), and, in principle, general openness in interaction with developers, but we will be realistic if the average system programmer will simply not like lsFusion less than SQL, ABAP and 1C is already success.

On the other hand, it is clear that in the near future the main market for lsFusion will be not system but application programming (the already mentioned development of IP), and now there are five main players: ERP platforms, SQL servers with procedural extensions, ORM frameworks , RAD frameworks, and just spreadsheets. The first, fourth and fifth types of platforms have a user interface in the kit; in the second and third, third-party technologies are used for this.

Each of these five types of platforms has its own niche, where they mainly live:

  • SQL servers with procedural extensions — business applications with relatively complex logic and large amounts of data — are usually retail and banks.
  • ERP-platforms - other business applications with complex logic - wholesale, manufacturing, finance, etc.
  • ORM-фреймворки – веб-приложения (сервисы, порталы), ну и очень высоконагруженные приложения с относительно несложной логикой.
  • RAD – узкоспециализированные низконагруженные бизнес-приложения с простой логикой, там где, как правило, сильно ограничен IT-бюджет.
  • Электронные таблицы – используются там же, где и RAD, правда, из-за низкого порога вхождения их можно встретить везде, где только можно, начиная от крупных корпораций и заканчивая полной автоматизацией малого бизнеса чисто на Excel (да, такое тоже встречается, и даже не знаю, какие ощущения это больше вызывает – восторг или страх).

In my purely subjective opinion, in the global perspective, lsFusion can completely replace the ERP, RAD and SQL platforms that lsFusion surpasses in all non-functional requirements (and in many of them it exceeds by an order of magnitude). True, with regard to SQL, it is more likely not about a replacement, but about an add-in, that is, just like, say, Fortran and C replaced the assembler (you can still write in assembler, but it is not clear why). With ORM frameworks, it will obviously be hard to compete in extreme flexibility and scalability, and with spreadsheets with entry thresholds in very simple tasks and in working with unstructured data. Although, nevertheless, it is possible that they will be able to win back some part of the market from them.

Well, in the medium term, the focus will mainly be on SMEs (which have limited human resources and IT budgets, but have great needs for flexibility and ergonomics of the solutions used), as well as non-standard tasks (where there are few ready-made solutions and their customization according to surpasses these solutions themselves). That is, to occupy the niche that 1C currently occupies in Russia, but only do it on a global scale.

This all, of course, sounds too ambitious, but after the path that has already been completed to just get all this technology to work (and it took almost 12 years), this task no longer seems so impossible.

UPD: The second part of the article can be found here .

Also popular now: