Diseñamos un lenguaje de programación multiparadigma. Parte 2 - Comparación de la construcción de modelos en PL / SQL, LINQ y GraphQL

En el ultimo postMe planteé la cuestión de que la lógica empresarial de los sistemas de información modernos incluye muchos elementos, cuyas descripciones son de naturaleza declarativa: la estructura de los conceptos, las relaciones entre ellos, las condiciones, las reglas, la transformación de conceptos al pasar de una capa de la aplicación a otra, su unificación, filtrado, agregación, etc. Desde mi punto de vista, los estilos funcionales y orientados a objetos son inferiores a los lógicos en cuanto a la conveniencia de la implementación de software del modelo de dominio. El estilo lógico transmite relaciones entre conceptos de una manera más compacta y natural. Por lo tanto, me propuse el objetivo de crear un lenguaje de programación híbrido que combinara un paradigma funcional o orientado a objetos con uno lógico. Además, el componente lógico debe ser conveniente para describir el modelo de dominio: la estructura de sus conceptos,así como las relaciones y dependencias entre ellos.



En esta publicación, quiero hablar sobre algunos lenguajes y tecnologías populares que incluyen elementos de programación declarativa: PL / SQL , MS LINQ y GraphQL . Trataré de averiguar qué tareas se resuelven en ellos utilizando programación declarativa, qué tan estrechamente se entrelazan los enfoques declarativo e imperativo, qué ventajas brinda y qué ideas se pueden aprender de ellos.



Extensiones de procedimiento SQL



Comencemos con un área en la que esta asociación se ha convertido durante mucho tiempo en un estándar de la industria: los lenguajes de acceso a datos. El más famoso de ellos es PL / SQL, una extensión de procedimiento del lenguaje SQL. Este lenguaje le permite procesar datos en una base de datos relacional utilizando tanto imperativos (variables, declaraciones de control, funciones, objetos) como estilos de programación declarativa (expresiones SQL). Usando una consulta SQL, podemos describir qué propiedades tienen los datos que necesitamos: qué campos se necesitan, de qué tablas tomarlos, cómo se relacionan entre sí, qué restricciones deben cumplir, cómo deben agregarse, etc. Y el servidor de la base de datos elaborará de forma independiente un plan de ejecución de consultas y buscará todos los conjuntos de campos posibles que cumplan las condiciones especificadas. La parte de procedimiento de PL / SQL le permite implementar esas tareasque son difíciles o imposibles de expresar en forma declarativa: procesar el resultado de una consulta en un bucle, realizar cálculos arbitrarios, estructurar el código en funciones y clases.



Los componentes procedimentales y declarativos del lenguaje están estrechamente integrados. PL / SQL le permite declarar funciones, ejecutar consultas dentro de ellas y devolver sus resultados, usar funciones dentro de una consulta, pasándoles los valores de los campos de la tabla como argumentos. Puede acceder a los resultados de una consulta utilizando cursores y luego recorrer imperativamente todos los registros recibidos. Los cursores le dan más control sobre el contenido de las tablas y le permiten implementar una lógica de procesamiento de datos mucho más compleja que usar SQL solo. Se puede asignar un cursor a una variable de cursor y pasar como argumento a funciones, procedimientos o incluso una aplicación cliente. El propio código de solicitud se puede generar dinámicamente mediante una secuencia de comandos imperativos.La combinación de procedimientos y consultas, mediante algunos ajustes, le permite implementar consultas recursivas. Incluso hay características orientadas a objetos en PL / SQL que le permiten declarar tipos de datos compuestos para campos de tabla, incluir métodos en ellos y crear clases a través de la herencia.



PL / SQL le permite implementar lógica empresarial en el lado del servidor de base de datos. Además, la implementación del modelo de dominio estará bastante cerca de su descripción. Los conceptos básicos del modelo de dominio se asignarán al modelo de datos relacionales. Los conceptos corresponderán a tablas, atributos, sus campos. Las restricciones en los valores de los campos se pueden incrustar en las descripciones de las tablas. Y las relaciones con otras tablas se pueden establecer mediante claves externas. Los conceptos abstractos construidos a partir de los básicos corresponderán a la vista. Se pueden utilizar en consultas junto con tablas, incluso para crear otras vistas. Las vistas se crean sobre la base de consultas, lo que le permite aprovechar todo el poder y la flexibilidad de SQL. De este modo,a partir de tablas y vistas, puede construir un modelo de dominio bastante complejo y de varios niveles completamente en un estilo declarativo. Y todo lo que no encaja bien en el estilo declarativo se puede implementar mediante procedimientos y funciones.



El principal problema es que el código PL / SQL se ejecuta exclusivamente en el lado del servidor DB. Esto hace que sea difícil escalar dicha solución. Además, el modelo resultante estará rígidamente vinculado a la base de datos relacional y será problemático incluir datos de otras fuentes en él.



Consulta integrada de lenguaje



Language Integrated Query (LINQ) es un componente popular de la plataforma .NET que le permite incluir de forma natural expresiones de consulta SQL en su código principal de lenguaje orientado a objetos. A diferencia de PL / SQL, que agrega un paradigma imperativo a SQL en el lado del servidor de bases de datos, LINQ lleva SQL al nivel de la aplicación. Gracias a esto, las consultas en LINQ se pueden utilizar para obtener datos no solo de bases de datos relacionales, sino también de colecciones de objetos, documentos XML y otras consultas LINQ.



La arquitectura LINQ es bastante flexible y las definiciones de consultas están profundamente integradas con el modelo OOP. LINQ le permite crear sus propios proveedores para acceder a nuevas fuentes de datos. También puede establecer su propia forma de ejecutar la consulta y, por ejemplo, convertir el árbol de expresión LINQ de la consulta en una consulta a la fuente de datos deseada. Puede usar expresiones lambda y funciones definidas en el código de su aplicación en el cuerpo de su solicitud. Es cierto que en el caso de LINQ to SQL, la consulta se ejecutará en el lado del servidor de la base de datos, donde estas funciones no estarán disponibles, pero en su lugar se pueden utilizar procedimientos almacenados. La solicitud es la esencia del lenguaje de primer nivel, puede trabajar con ella como con un objeto ordinario. El compilador puede inferir automáticamente el tipo de resultado de la consulta y generar la clase adecuada, incluso si no se ha declarado explícitamente.



Intentemos usar LINQ para construir un modelo de dominio como un conjunto de consultas. Los hechos originales se pueden colocar en listas en el lado de la aplicación o en tablas en el lado de la base de datos, y los conceptos abstractos se pueden formatear como consultas LINQ. LINQ le permite crear consultas basadas en otras consultas especificándolas en la cláusula FROM. Esto le permite construir un nuevo concepto basado en los existentes, los campos en la sección SELECT corresponderán a los atributos del concepto. Y la sección DONDE contendrá dependencias entre conceptos. Un ejemplo con facturas de una publicación anterior se verá así.



Colocaremos objetos con cuentas e información de clientes en las listas:



List<Bill> bills = new List<Bill>() { ... };
List<Client> clients = new List<Client>() { ... };


Y luego crearemos consultas para que reciban facturas impagas y deudores:



IEnumerable<Bill> unpaidBillsQuery =
from bill in bills
where bill.AmountToPay > bill.AmountPaid 
select bill;
IEnumerable<Client> debtorsQuery =
from bill in unpaidBillsQuery 
join client in clients on bill.ClientId equals client.ClientId
select client;


El modelo de dominio implementado usando LINQ ha adquirido una forma bastante extraña: varios estilos de programación están involucrados a la vez. El nivel superior del modelo tiene semántica imperativa. Se puede representar como cadenas de transformaciones de objetos, construyendo colecciones de objetos sobre colecciones. Los objetos de consulta son elementos del mundo OOP. Deben crearse, asignarse a variables y las referencias a ellas deben pasarse a otras solicitudes. En el nivel medio, el objeto de consulta implementa el procedimiento para ejecutar una consulta, que se personaliza funcionalmente con expresiones lambda que le permiten formar la estructura de resultados en la sección SELECT y filtrar registros en la cláusula WHERE. El nivel interno está representado por el procedimiento de ejecución de consultas, que tiene semántica lógica y se basa en álgebra relacional.



Aunque LINQ hizo posible describir el modelo de dominio, la sintaxis SQL está dirigida principalmente a buscar y manipular datos. Carece de algunos constructos que serían útiles en el modelado. Si en PL / SQL la estructura de los conceptos básicos estaba muy claramente representada en forma de tablas y vistas, entonces en LINQ resultó estar representada en código OOP. Además, mientras que las tablas y vistas pueden ser referenciadas por su nombre, las consultas LINQ pueden ser referenciadas en un estilo imperativo. Además, SQL está limitado por el modelo relacional y tiene capacidades limitadas cuando se trabaja con estructuras en forma de gráficos o árboles.



Paralelos entre el modelo relacional y la programación lógica



Puede ver que las implementaciones de SQL y Prolog del modelo tienen similitudes. En SQL, creamos una vista basada en tablas u otras vistas, y en Prolog creamos reglas basadas en hechos y reglas. En SQL, las tablas son una colección de campos y los predicados en Prolog son una colección de atributos. En SQL, especificamos dependencias entre campos de tabla como expresiones en la cláusula WHERE y en Prolog, utilizando predicados y variables booleanas que vinculan atributos de predicado entre sí. En ambos casos, establecemos declarativamente la especificación de la solución, y el motor de ejecución de consultas integrado nos devuelve los registros encontrados en SQL o los posibles valores de las variables en Prolog.



Esta similitud no es accidental. Aunque la base teórica de SQL - álgebra relacional se desarrolló en paralelo con la programación lógica, más tarde se reveló una conexión teórica entre ellos. Tienen una base matemática común: lógica de primer orden. El modelo de datos relacionales describe las reglas para construir relaciones entre tablas de datos, programación lógica, entre declaraciones. Ambas teorías utilizan términos diferentes, se aplican en diferentes campos, se desarrollaron en paralelo, pero tenían una base matemática común.



Estrictamente hablando, el cálculo relacional es una adaptación de la lógica de primer orden para trabajar con datos tabulares. Esta cuestión se analiza con más detalle aquí.... Es decir, cualquier expresión de álgebra relacional (cualquier consulta SQL) puede reformularse en una expresión de lógica de primer orden y luego implementarse en Prolog. Pero no al revés. El cálculo relacional es un subconjunto de la lógica de primer orden. Esto significa que para algunos tipos de enunciados que son admisibles en la lógica de primer orden, no podemos encontrar analogías en el álgebra relacional. Por ejemplo, las capacidades de las consultas recursivas en SQL son muy limitadas y la construcción de relaciones transitivas tampoco siempre está disponible. Las operaciones de prólogo como la disyunción de destino y la negación como el rechazo son mucho más difíciles de implementar en SQL. La sintaxis flexible de Prolog le brinda más flexibilidad para trabajar con estructuras anidadas complejas y admite operaciones de coincidencia de patrones en ellas.Esto lo hace conveniente cuando se trabaja con estructuras de datos complejas, como árboles y gráficos.



Pero tienes que pagar por todo. Los algoritmos de ejecución de consultas integrados en las bases de datos relacionales son más simples y menos versátiles que los algoritmos de inferencia en Prolog. Esto hace posible optimizarlos y lograr un rendimiento mucho mayor. Prolog tampoco puede procesar rápidamente millones de filas en bases de datos relacionales. Además, el algoritmo de inferencia de Prolog no garantiza el final de la ejecución del programa en absoluto: la salida de algunas declaraciones puede conducir a una recursividad infinita.



Por cierto, en la intersección de las bases de datos y la programación lógica, también existe la tecnología como las bases de datos deductivas y el lenguaje de reglas y consultas a ellos Datalog. En lugar de registros en tablas, las bases de datos deductivas almacenan grandes cantidades de hechos y reglas en un estilo lógico. Y Datalog se parece a Prolog, pero se centra en trabajar con hechos combinados en conjuntos, no hechos individuales. Además, se cortaron algunas características de la lógica de primer orden con el fin de optimizar el algoritmo de inferencia para un trabajo rápido con grandes cantidades de datos. Por tanto, la sintaxis menos expresiva de un lenguaje lógico también tiene sus ventajas.



Enfoque declarativo para la descripción de la capa de API



SQL vincula la construcción del modelo a la capa de acceso a datos. Pero la programación declarativa también se está desarrollando activamente en el extremo opuesto de la aplicación, en la capa API. Su peculiaridad es que la información sobre la estructura de las solicitudes debe estar disponible para quienes utilizan esta API. Tener una descripción formal de la estructura de solicitudes y respuestas es una buena forma. Por consiguiente, existe el deseo de sincronizar esta descripción con el código de la aplicación, por ejemplo, generar clases de solicitud y respuesta basadas en él. En el que luego deberá escribir la lógica para procesar las solicitudes.



GraphQL es un marco para crear API que va mucho más allá de este enfoque tradicional y ofrece no solo un lenguaje de consulta, sino también un entorno de ejecución de consultas. No es necesario generar código, el tiempo de ejecución comprende las descripciones de las solicitudes de todos modos. Para implementar la API usando GraphQL, necesita:



  1. describir los tipos de datos (objetos) de la aplicación que forman parte de las solicitudes y respuestas;
  2. describir la estructura de solicitudes y respuestas;
  3. Implementar funciones que implementen la lógica de crear objetos para obtener los valores de sus campos.


Los tipos de datos son descripciones de campos de objetos. Se admiten tipos como tipos escalares, listas, enumeraciones y referencias a tipos anidados. Dado que los campos de tipo pueden contener referencias a otros tipos, el esquema de datos completo se puede representar como un gráfico. La solicitud es una descripción de la estructura de datos solicitada a la API. La descripción de la solicitud incluye una lista de objetos obligatorios, sus campos y atributos de entrada. Cada tipo de datos y cada uno de sus campos deben estar asociados con una función de resolución. El resolutor de tipo (objeto) describe el algoritmo para obtener sus objetos, el resolutor de campo describe los valores del campo del objeto. Representan funciones en uno de los lenguajes funcionales u orientados a objetos. El tiempo de ejecución de GraphQL recibe una solicitud, determina los tipos de datos requeridos, llama a sus resolutores, incluso a lo largo de una cadena de objetos anidados,recopila un objeto de respuesta.



GraphQL combina la descripción de esquemas de datos declarativos con algoritmos imperativos o funcionales para obtenerlos. El esquema de datos se describe explícitamente y es fundamental para la aplicación. Mucha gente señala que es una buena práctica crear un esquema de datos que no duplique los esquemas de origen de datos, sino que se ajuste al modelo de dominio. Esto hace que GraphQL sea una solución bastante popular para integrar fuentes de datos dispares.



Por lo tanto, el lenguaje GraphQL le permite expresar el modelo de dominio de una manera bastante clara, para distinguirlo del resto del código, para acercar el modelo y su implementación. Desafortunadamente, el componente declarativo del lenguaje se limita solo a la descripción de la composición de los tipos de datos; todas las demás relaciones entre los elementos del modelo deben implementarse utilizando resolutores. Por un lado, los resolutores permiten a un desarrollador implementar de forma independiente cualquier método de obtención de datos para un objeto y cualquier relación entre ellos. Pero, por otro lado, tendrás que intentar implementar opciones de consulta más complejas que, por ejemplo, el acceso a un registro por clave. Por un lado, el esquema de datos en GraphQL muestra claramente la relación entre la capa de API y la capa de acceso a datos. Pero, por otro lado, la capa principal a la que está vinculado el esquema de datos es la capa API.El contenido del esquema de datos se ajusta a él; no contendrá entidades que no estén involucradas en el procesamiento de solicitudes. Aunque el poder expresivo del lenguaje de descripción de datos GraphQL es inferior a los lenguajes declarativos completos como SQL y Prolog, la popularidad de este marco muestra que las herramientas para la descripción de modelos declarativos pueden y deben ser parte de los lenguajes de programación modernos.



Voy a resumir



PL / SQL es un lenguaje conveniente tanto para describir un modelo de dominio en forma de tablas y vistas, como para la lógica de trabajar con él. Los componentes declarativos y procedimentales están estrechamente integrados y son complementarios. El principal problema es que este lenguaje está estrechamente relacionado con la ubicación de almacenamiento de datos, solo se puede ejecutar en el lado del servidor de la base de datos y la lógica de ejecución de consultas se limita al modelo de datos relacionales.



En el lado de la aplicación, puede utilizar tecnologías como LINQ y GraphQL para describir el modelo en forma declarativa. Usando el esquema de datos GraphQL, puede describir clara y muy claramente la estructura del modelo de dominio, el anidamiento de sus conceptos. Y el tiempo de ejecución es capaz de recopilar automáticamente los objetos necesarios. Desafortunadamente, todas las demás relaciones y conexiones entre conceptos, excepto su anidamiento, deben implementarse en la capa de funciones de resolución. LINQ tiene ventajas y desventajas opuestas. La sintaxis SQL flexible le brinda más flexibilidad para describir las relaciones entre conceptos. Pero fuera de la solicitud, la declaratividad termina, los objetos de solicitud son elementos del mundo OOP. Deben crearse, asignarse a variables y usarse en un estilo imperativo.



Me gustaría combinar las ventajas de LINQ y GraphQL. De modo que la descripción de la estructura de conceptos fuera clara como en GraphQL, y las relaciones entre ellos se pudieran establecer en base a la lógica como en SQL. Y para que las definiciones de conceptos estén disponibles directamente por nombre como clases, sin la necesidad de crear explícitamente sus objetos, asignarlos a variables, pasarles referencias, etc.



Comenzaré a diseñar tal solución desarrollando un lenguaje para describir un modelo de dominio. Pero para ello es necesario hacer un repaso de los lenguajes de representación del conocimiento existentes. Por eso, en la próxima publicación quiero hablar sobre programación lógica, RDF, OWL y lenguajes de lógica de marco, compararlos y tratar de encontrar características que sean interesantes para el lenguaje diseñado para describir la lógica empresarial.



Para aquellos que no quieran esperar el lanzamiento de todas las publicaciones sobre Habré, hay un texto completo en estilo científico en inglés, disponible en el enlace: Programación híbrida orientada a la ontología para el procesamiento de datos semiestructurados .



Enlaces a publicaciones anteriores:

Diseño de un lenguaje de programación multiparadigma. Parte 1 - ¿Para qué sirve?



All Articles