No es otro lenguaje de programación. Parte 1: lógica de dominio



Recientemente, una gran cantidad de nuevos lenguajes de programación han aparecido en el mercado: Go, Swift, Rust, Dart, Julia, Kotlin, Hack, Bosque, y este es solo uno de los que se escuchan.
El valor de lo que estos lenguajes aportan al mundo de la programación es difícil de sobreestimar, pero como Y Combinator señaló el año pasado cuando hablaba de herramientas de desarrollo:
Los marcos están mejorando, los idiomas son un poco más inteligentes, pero básicamente hacemos lo mismo.
Este artículo hablará sobre un lenguaje basado en un enfoque que es fundamentalmente diferente de los enfoques utilizados en todos los idiomas existentes, incluidos los enumerados anteriormente. En general, este lenguaje puede considerarse un lenguaje de propósito general, aunque algunas de sus capacidades y la implementación actual de la plataforma construida sobre él, sin embargo, probablemente limitan su aplicación a un área un poco más estrecha: el desarrollo de sistemas de información.

Haré una reserva de inmediato, no se trata de una idea, un prototipo o incluso MVP, sino de un lenguaje completo y listo para la producción con todo el lenguaje de infraestructura necesario, desde el entorno de desarrollo (con un depurador) hasta el soporte automático de varias versiones del lenguaje (con correcciones de errores de fusión automática entre ellos) , nota de lanzamiento, etc.). Además, utilizando este lenguaje, ya se han implementado varias docenas de proyectos de complejidad del nivel ERP, con cientos de usuarios simultáneos, bases de datos de terabytes, "fechas límite de ayer", presupuestos limitados y desarrolladores sin experiencia en TI. Y todo esto al mismo tiempo. Bueno, por supuesto, debe tenerse en cuenta que ahora no es el año 2000, y todos estos proyectos se implementaron sobre los sistemas existentes (que no estaban allí), lo que significa que al principio era necesario hacer "como estaba" gradualmente, sin detener el negocio, y luego, También gradualmente, haga lo que debe ser. En general, así es como se venden los primeros autos eléctricos no a los hipsters ricos de California, sino a los servicios de taxi de bajo costo en algún lugar de Omsk.

Se lanza una plataforma construida en este idioma bajo la licencia LGPL v3. Honestamente, no quería escribirlo bien en la introducción, ya que esto está lejos de ser la ventaja más importante, pero, hablando con personas que trabajan en uno de sus principales mercados potenciales: plataformas ERP, noté una característica: todas estas personas, sin excepción, dicen que incluso si haces lo mismo que ya está en el mercado, pero de forma gratuita, entonces ya será genial. Así que déjalo aquí.

Poco de teoría


Comencemos con la teoría para resaltar la diferencia en los enfoques fundamentales utilizados en este y otros lenguajes modernos.

Un pequeño descargo de responsabilidad, un razonamiento adicional hasta cierto punto es un intento de tirar de un búho en un globo terráqueo, pero con una teoría fundamental en la programación, en principio, digamos sin rodeos, no realmente, por lo que debe usar lo que tiene.

Una de las primeras y principales tareas resueltas por la programación es el problema de calcular los valores de las funciones. Desde el punto de vista de la teoría computacional, hay dos enfoques fundamentalmente diferentes para resolver este problema.

El primer enfoque de este tipo son varias máquinas (la más famosa de las cuales es una máquina de Turing), un modelo que consiste en el estado actual (memoria) y una máquina (procesador), que en cada paso cambia este estado actual de una forma u otra. Este enfoque también se llama la arquitectura de Von Neumann, y es él quien subyace a todas las computadoras modernas y al 99 por ciento de los idiomas existentes.

El segundo enfoque se basa en el uso de operadores; lo utilizan las llamadas funciones parcialmente recursivas (en adelante, ChRF). Además, la diferencia más importante entre este enfoque no está en el uso de operadores como tales (los operadores, por ejemplo, también están en la programación estructural usando el primer enfoque), sino en la posibilidad de iterar sobre todos los valores de la función (ver el operador para minimizar el argumento) y en ausencia de estado en proceso de calculo.

Al igual que la máquina Turing, las funciones parcialmente recursivas son Turing completas, es decir, se pueden usar para especificar cualquier cálculo posible. Aquí, aclaramos de inmediato que tanto la máquina Turing como el CRF son solo bases mínimas, y luego nos enfocaremos en ellos como enfoques, es decir, en un modelo con memoria de procesador y en un modelo con operadores sin usar variables y la posibilidad de iterar sobre todos los valores funciones respectivamente.

El CRF como enfoque tiene tres ventajas principales:

  • Está mucho mejor optimizado. Esto se aplica tanto directamente a la optimización del proceso de cálculo del valor, como a la posibilidad de paralelismo de dicho cálculo. En el primer enfoque, el efecto secundario, por el contrario, introduce una gran complejidad en estos procesos.
  • Se incrementa mucho mejor, es decir, para una función construida, puede ser mucho más eficiente determinar cómo cambiarán sus valores cuando cambien los valores de las funciones que utiliza esta función construida. Estrictamente hablando, esta ventaja es un caso especial del primero, pero es precisamente esto lo que ofrece una gran cantidad de posibilidades, que básicamente no pueden estar en el primer enfoque, por lo tanto, se destaca como un elemento separado.
  • Es mucho más fácil de entender. Es decir, en términos generales, la descripción de la función de calcular la suma de un indicador en el contexto de otros dos indicadores es mucho más fácil de entender que si la misma se describe en términos del primer enfoque. Sin embargo, en problemas algorítmicamente complejos la situación es diametralmente opuesta, pero vale la pena señalar que los problemas algorítmicamente complejos en la gran mayoría de las áreas son buenos si son del 5%. En general, para resumir un poco, el CRF es matemática, y las máquinas de Turing son ciencias de la computación. En consecuencia, las matemáticas se estudian casi en el jardín de infantes, y la informática es opcional y desde la escuela secundaria. La comparación regular, por supuesto, pero aún da algún tipo de métrica en este asunto.

Las máquinas de Turing tienen al menos dos ventajas:

  • Ya mencioné la mejor aplicabilidad en problemas algorítmicamente complejos
  • Todas las computadoras modernas se basan en este enfoque.

Además, en esta comparación solo estamos hablando de tareas de cálculo de datos; en problemas de cambio de datos sin máquinas Turing, aún no puede hacerlo.

Después de leer en este lugar, cualquier lector atento hará una pregunta razonable: "Si el enfoque CRF es tan bueno, ¿por qué no se utiliza en un lenguaje moderno común?" Entonces, de hecho, esto no es así, se usa, además, en el lenguaje que se usa en la gran mayoría de los sistemas de información existentes. Como puede suponer, este lenguaje es SQL. Aquí, por supuesto, el mismo lector atento objetará razonablemente que SQL es el lenguaje del álgebra relacional (es decir, trabajar con tablas, no funciones), y será correcto. Formalmente De hecho, podemos recordar que las tablas en el DBMS generalmente están en la tercera forma normal, es decir, tienen columnas clave, lo que significa que cualquier columna restante de esta tabla puede considerarse como una función de sus columnas clave. No es obvio, francamente. Y por qué SQL no ha crecido de un lenguaje de álgebra relacional a un lenguaje de programación completo (es decir, trabajar con funciones) es una gran pregunta. En mi opinión, hay muchas razones para esto, la más importante de las cuales es "una persona rusa (en realidad cualquiera) no puede trabajar con el estómago vacío, pero no quiere trabajar con una bien alimentada", en el sentido de que, como lo demuestra la práctica, el trabajo necesario para esto es realmente titánico y conlleva demasiados riesgos para las pequeñas empresas, y para las grandes empresas, en primer lugar, todo está bien y, en segundo lugar, es imposible forzar este trabajo con dinero: la calidad es importante aquí, no la cantidad. En realidad, la ilustración más vívida de lo que sucede cuando las personas intentan resolver un problema por cantidad en lugar de calidad es Oracle, que incluso logró implementar incluso la aplicación más básica de incrementalidad: representaciones materializadas actualizadas para que este mecanismo tenga varias restricciones de varias páginas (justicia) en aras de eso, Microsoft es aún peor ). Sin embargo, esta es una historia separada, tal vez habrá un artículo separado al respecto.

Al mismo tiempo, no es que SQL sea malo. No En su nivel de abstracción, realiza sus funciones perfectamente, y la implementación actual de la plataforma la utiliza un poco menos que completamente (en cualquier caso, mucho más que todas las demás plataformas). Otra cosa es que inmediatamente después de su nacimiento, SQL realmente se detuvo en el desarrollo y no se convirtió en lo que podría convertirse, es decir, el lenguaje que se discutirá ahora.

Pero suficiente teoría, es hora de ir directamente al idioma.

Entonces, nos encontramos con :


Específicamente, este artículo será la primera parte de tres (ya que aún hay demasiado material incluso para dos artículos), y solo hablará sobre el modelo lógico, es decir, solo sobre lo que está directamente relacionado con la funcionalidad del sistema y no está relacionado con los procesos. Desarrollo e implementación (optimización del rendimiento). Además, hablaremos solo de una de las dos partes del modelo lógico: la lógica del área temática. Esta lógica determina qué información almacena el sistema y qué puede hacer con esta información (cuando se desarrollan aplicaciones empresariales, a menudo también se la denomina lógica empresarial).

Gráficamente, todos los conceptos de lógica de dominio en lsFusion se pueden representar mediante la siguiente imagen:


Las flechas en esta imagen indican las instrucciones de uso de los conceptos entre sí, por lo tanto, los conceptos forman una especie de pila y, en consecuencia, es en el orden de esta pila que hablaré sobre ellos.


Las propiedades


Una propiedad es una abstracción que toma uno o más objetos como parámetros y devuelve algún objeto como resultado. La propiedad no tiene efectos secundarios y, de hecho, es una función pura, sin embargo, a diferencia de esta última, no solo puede calcular valores, sino también almacenarlos. En realidad, el nombre "propiedad" en sí se toma prestado de otros lenguajes de programación modernos, donde se usa aproximadamente para los mismos fines, pero se enclava en la encapsulación y, en consecuencia, solo se admite para funciones con un parámetro. Bueno, el hecho de que esta misma palabra "propiedad" sea más corta que "función pura", además de que no tiene asociaciones innecesarias, se juega a favor del uso de este término en particular.

Las propiedades se establecen de forma recursiva utilizando un conjunto predefinido de operadores. Hay muchos de estos operadores, por lo tanto, consideramos solo los principales (estos operadores cubren el 95% de cualquier proyecto estático promedio).

Propiedad primaria (DATOS)


La propiedad primaria es una propiedad cuyo valor se almacena en la base de datos y se puede cambiar como resultado de la acción correspondiente (un poco más adelante). Por defecto, el valor de cada una de esas propiedades para cualquier conjunto de parámetros es igual a un valor NULL especial.
quantity = DATA INTEGER (Item);
isDayOff = DATA BOOLEAN (Country, DATE);
, ( ), .

. :

class X { 	
    Y y; 	
    Map<Y, Z> f; 	
    Map<Y, Map<M, Z>> m; 	
    List<Y> n;
    LinkedHashSet<Y> l; //   
    static Set<Y> s;
}

:
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);

(JOIN), , (+,-,/,*), (AND, OR), (+, CONCAT), (>,<,=), (CASE, IF), (IS)

f(a) = IF g(h(a)) > 5 AND a IS X THEN ‘AB’ + ‘CD’ ELSE x(5);
- , . , , , :

  • , . , ( NULL). , lsFusion – , , , – TRUE ( FALSE NULL), 3-state’.
  • NULL: (+), (-), CONCAT . :
    • : NULL 0, – , 0 NULL ( 5 (+) NULL = 5, 5 (-) 5 = NULL, 5 + NULL = NULL 5 — 5 = 0).
    • : NULL ( CONCAT ‘ ‘, ‘John’,’Smith’ = ‘John Smith’, CONCAT ‘ ‘, ‘John’, NULL = ‘John’, ‘John’ + ‘ ‘ + NULL = NULL).
  • (IF) ( ) : f(a) IF g(a), f(a) g(a) NULL, NULL – .

(GROUP)


– . (, ) .

:

  • :
    sum(Invoice i) = GROUP SUM sum(InvoiceDetail id) IF invoice(id) = i;
    currentBalance(Sku sk) = GROUP SUM currentBalance(sk, Stock st);
    , ( i sk). , , - :
    x() = (GROUP SUM f(a)) + 5;
  • SQL-:
    sum = GROUP SUM sum(InvoiceDetail id) BY invoice(id);
    currentBalance = GROUP SUM currentBalance(Sku sk, Stock st) BY sk;
    ( , , )

, ( ), – ( ). , , , ( , , SQL – ). , ( BY), - :
// BY     ,   s
sum(DATE from, Stock s, DATE to) = GROUP sum(Invoice i) IF date(i) >= from AND date(i) <=to BY stock(i); 
, , , , , .

:

  • /,

  • .

/ (PARTITION … ORDER)


( , ) , . , ( , ). / .
place(Team t) = PARTITION SUM 1 ORDER DESC points(t) BY conference(t);
, , , , , , .

SQL ( , ) (OVER PARTITION BY… ORDER BY).

(RECURSION)


– , . , , .

. , ( ):

  • (result) ( ) :
    • result(0, o1, o2, ..., oN) = initial(o1, ..., oN), initial –
    • result(i+1, o1, o2, ..., oN) = step(o1, ..., oN, $o1, $o2, ..., $oN) IF result(i, $o1, $o2, ..., $oN), step – .
  • , ( o1, o2, …, oN). , .

, , , , :
//   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=1STEP 1 IF (i=$i+1 OR i=$i+2AND i<to CYCLES IMPOSSIBLE;
, / , , , .

, , , , , lsFusion – .

SQL CTE, , , . , Postgres GROUP BY , , , , , , . , , WHILE’ .

. , , .


– , , , , ( , , ). , , “”, , -, , , -, , “”.

, – lsFusion. , – . , – ( , , , ). , – . .

, . / , , , , , . 3 97, lsFusion – 60 40.

, , . , -, ( , ), .

, :

(FOR), (WHILE)


, lsFusion , , NULL ( ).
FOR selected(Team team) DO
    MESSAGE 'Team ' + name(team) + ' was selected';
, :
showAllDetails(Invoice i) {
    FOR invoice(InvoiceDetail id) = i ORDER index(id) DO
        MESSAGE 'Sku : ' + nameSku(id) + ', ' + quantity(id);
}
, , (IF).

(WHILE) , :

  • , NULL ( )


(EXEC), ({…}), (CASE, IF), (BREAK), (RETURN)

f(a) {
    FOR t=x(b,a) DO {
        do(b);
        IF t>5 THEN
            BREAK;
    }
    MESSAGE 'Succeeded';
}
- . , , .

(CHANGE)


. , , , , NULL. :
//       
setDiscount(Customer c)  {
    discount(c, Item i) <- 15 WHERE selected(i);
}
, :
setDiscount(Customer c)  {
    FOR selected(Item i) DO
        discount(c, i) <- 15;
}
, , , ( , , , , selected discount – ), . , , .

(NEW)


( , , , , ). , , .

:
newSku ()  {
    LOCAL addedSkus = Sku (INTEGER);
    NEW Sku WHERE iterate(i, 13TO addedSkus(i);
    FOR Sku s = addedSkus(i) DO {
        id(s) <- 425;
        name(s) <- 'New Sku : ' + i;
    }
}
, , — NEW (FOR), ( ):
FOR iterate(i, 13NEW s=Sku DO  {
    id(s) <- i;
    name(s) <- 'New Sku : ' + i;
}
, FOR :
NEW s=Sku DO {
    id(s) <- 425;
    name(s) <- 'New Sku';
}
– , , , .

(DELETE)


– :
DELETE Sku s WHERE name(s) = 'MySku';
, «» .

, , .


, , . , , . .

, . , — / .

, , . , ( ), .

, , . , :
LOCAL f = INTEGER (INTEGERINTEGER);

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
: (APPLY) (CANCEL). , , , . , , , . – , , , , . — .

(NEWSESSION, NESTEDSESSION)


(, , http- ..). , , . , , , «», «» ( , ). NEWSESSION, ( ). :
run() {
    f(1) <- 2;
    APPLY;
    f(1) <- 1;
    NEWSESSION {
        MESSAGE f(1); //  2,       
        f(2) <- 5;
        APPLY;          
    }
    MESSAGE f(1); //  1,     1  ,   
}
, , , . , :
run(Store s) {
    NEWSESSION
        MESSAGE 'I see that param, its name is: ' + name(s);
}
, , ( ). NESTED, , , , . , ( , NESTED). :
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
}
. :

  • , , , < —
  • , , : < —
  • , : < — .

, (, ), , . , , - , , , , , :


  • ,

(APPLY), (CANCEL)


– , . , . , :

  • . , , , NESTED ( ).
  • , . , - , , , . , (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: Authentication, , , ( ) ( ). , , lsFusion , (, ).

– , , , , ( ).

(PREV, CHANGED, SET, DROPPED)


: (PREV), (CHANGED), NULL NULL (SET) .. ( ), , , :
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
}
. , , , , .


“ ?”, “ ?”. , , .

, , . , . .

:

  • – .
  • – , / .

, , :

  • – .
  • – .

, , , .

:

  • , , , .
  • , .

:

  • , « ». , , , ( , , 5-10 , ).
  • , ( ), , , , .

:

  • , , .

:

  • , , ( ), , .

( , ), .
ON { //   ,       APPLY
    MESSAGE 'Something changed';
}
, , , , ‘Something changed’ (!) ( , ). , , - , (CHANGED, SET, DROPPED ..). , - , -, -. – :
//  email,          
WHEN SET(balance(Sku s, Stock st) < 0DO
      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;

, – , . , :
ON {
    FOR SET(balance(Sku s, Stock st) < 0DO
        EMAIL SUBJECT '     ' + name(s) + '   ' + name(st);
}
, , / , , .

SQL ( ) . , , , ( ) , .

, :

  • – , ( )
  • – , . , , «» .


, , . , , 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 '        ';
, – , NULL (SET) , – NULL . – , , , / , .

, – ( ), , , ( ), , .


. , , , – . , :
f = DATA A (INTEGER);
, f NULL , A. :
f = Object (INTEGER);
CONSTRAINT f(i) AND NOT f(i) IS A MESSAGE ' '// f(i) => f(i) IS A
, , – . ( , , «»), , , , : - , .

, lsFusion . , lsFusion . , . lsFusion , , - :
CLASS A {
    f = DATA LONG (INTEGER); //  f = DATA LONG (A, INTEGER)
}
lsFusion , :
CLASS Animal;
CLASS Transport;
CLASS Car : Transport;
CLASS Horse : Transport, Animal;
, – .


lsFusion – . , , :
speed = ABSTRACT LONG (Transport);
/ , :
CLASS Breed;
speed = DATA LONG (Breed)
breed = DATA Breed (Animal);

speed(Horse h) += speed(breed(h)); //       
( ):
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);
}
, , ( ), . ABSTRACT :
speed(Transport t) = CASE 
    WHEN t IS Horse THEN speed(breed(t))
    //  
END
, .

, , :
speed(Horse h) = speed(breed(h));
, , ( ). , , .


, , . () : , , . , , , , , . NULL , , :
f = DATA LONG (LONG);
g = DATA LONG (A);
h(a) = OVERRIDE f(a), g(a); //   


( ) – , . , , , :
CLASS Direction '' {
    left '',
    right '',
    forward ''
}

result(dir) = CASE
    WHEN dir = Direction.left THEN ' '
    WHEN dir = Direction.right THEN ' '
    WHEN dir = Direction.forward THEN ' '
END
, .

enum’, , .


( lsFusion, ) :

  • ( ).
  • ( ).
  • .

.

() , NULL . , , , , , .

:
//   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 
, , ( ):
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
, (, ), ( ) , . , ERP 1100 . , , , . , ( 50 ), , 1100 300 .

, , , , . , , – , – , , , « - », , , .


, lsFusion . , , , , , , , , , ( , , ). , lsFusion ( ACID), , SQL-, . , DSL lsFusion , , – – . , SQL , , lsFusion . , , – , github ( , ), (IDE, , VCS, ), slack telegram- , (linux maven, - ), , , , , lsFusion , SQL, ABAP 1 – .

, , lsFusion, , ( ), : ERP-, SQL- , ORM-, RAD-, . , , .

, :

  • SQL- – - – , , .
  • ERP- – - – , , ..
  • ORM- – - (, ), .
  • RAD – - , , , IT-.
  • – , RAD, , - , , Excel (, , , – ).

, lsFusion ERP, RAD SQL , lsFusion ( ). , SQL, , , , , , Fortran C ( , ). ORM- , – . , , - .

SME ( , ), ( ). , 1 , .

, , , , , ( 12 ), .

UPD: .

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


All Articles