Nós projetamos uma linguagem de programação multiparadigma. Parte 2 - Comparação de construção de modelo em PL / SQL, LINQ e GraphQL

Na última postagemLevantei a questão de que a lógica de negócios dos sistemas de informação modernos inclui muitos elementos, cujas descrições são declarativas por natureza: a estrutura dos conceitos, as relações entre eles, condições, regras, a transformação de conceitos ao passar de uma camada do aplicativo para outra, seus união, filtragem, agregação, etc. Do meu ponto de vista, os estilos funcionais e orientados a objetos são inferiores aos lógicos em termos da conveniência de implementação de software do modelo de domínio. O estilo lógico transmite relacionamentos entre conceitos de uma forma mais compacta e natural. Portanto, eu me propus a meta de criar uma linguagem de programação híbrida que combinaria um paradigma orientado a objetos ou funcional com um paradigma lógico. Além disso, o componente lógico deve ser conveniente para descrever o modelo de domínio - a estrutura de seus conceitos,bem como relacionamentos e dependências entre eles.



Neste post, quero falar sobre algumas linguagens e tecnologias populares que incluem elementos de programação declarativa - PL / SQL , MS LINQ e GraphQL . Tentarei descobrir quais tarefas são resolvidas neles usando programação declarativa, quão estreitamente as abordagens declarativa e imperativa estão interligadas, quais vantagens isso oferece e quais ideias podem ser aprendidas com elas.



Extensões de procedimento SQL



Vamos começar com uma área em que essa associação há muito se tornou um padrão do setor - linguagens de acesso a dados. O mais famoso deles é o PL / SQL, uma extensão procedural da linguagem SQL. Esta linguagem permite processar dados em um banco de dados relacional usando estilos de programação imperativos (variáveis, instruções de controle, funções, objetos) e declarativos (expressões SQL). Usando uma consulta SQL, podemos descrever quais propriedades os dados de que precisamos - quais campos são necessários, de quais tabelas obtê-los, como eles estão relacionados entre si, quais restrições devem obedecer, como devem ser agregados, etc. E o servidor de banco de dados elaborará de forma independente um plano de execução de consulta e encontrará todos os conjuntos possíveis de campos que atendam às condições especificadas. A parte procedural do PL / SQL permite que você implemente essas tarefasque são difíceis ou impossíveis de expressar em uma forma declarativa - processar o resultado de uma consulta em um loop, realizar cálculos arbitrários, estruturar o código em funções e classes.



Os componentes procedurais e declarativos da linguagem são totalmente integrados. A PL / SQL permite declarar funções, executar consultas dentro delas e retornar seus resultados, usar funções dentro de uma consulta, passando-lhes os valores dos campos da tabela como argumentos. Você pode acessar os resultados de uma consulta usando cursores e, em seguida, obrigatoriamente percorrer todos os registros recuperados. Os cursores fornecem mais controle sobre o conteúdo das tabelas e permitem implementar uma lógica de processamento de dados muito mais complexa do que usar apenas SQL. Um cursor pode ser atribuído a uma variável de cursor e passado como um argumento para funções, procedimentos ou mesmo um aplicativo cliente. O próprio código de solicitação pode ser gerado dinamicamente por uma sequência de comandos imperativos.A combinação de procedimentos e consultas, usando alguns ajustes, permite implementar consultas recursivas. Existem até recursos orientados a objetos no PL / SQL que permitem declarar tipos de dados compostos para campos de tabela, incluir métodos neles e criar classes por meio de herança.



A PL / SQL permite que você implemente a lógica de negócios no servidor de banco de dados. Além disso, a implementação do modelo de domínio estará bastante próxima de sua descrição. Os conceitos básicos do modelo de domínio serão mapeados para o modelo de dados relacional. Os conceitos corresponderão a tabelas, atributos - seus campos. Restrições em valores de campo podem ser incorporadas em descrições de tabela. E os relacionamentos com outras tabelas podem ser definidos usando chaves estrangeiras. Os conceitos abstratos construídos com base nos de base corresponderão à vista. Eles podem ser usados ​​em consultas junto com tabelas, inclusive para construir outras visualizações. As visualizações são construídas com base em consultas, permitindo que você aproveite todo o poder e flexibilidade do SQL. Nesse caminho,a partir de tabelas e visualizações, você pode construir um modelo de domínio bastante complexo e multinível completamente em um estilo declarativo. E tudo o que não se encaixa bem no estilo declarativo pode ser implementado usando procedimentos e funções.



O principal problema é que o código PL / SQL é executado exclusivamente no servidor de banco de dados. Isso torna difícil dimensionar essa solução. Além disso, o modelo resultante será rigidamente ligado ao banco de dados relacional e será problemático incluir dados de outras fontes nele.



Consulta Integrada de Linguagem



A Consulta Integrada à Linguagem (LINQ) é um componente popular da plataforma .NET que permite incluir naturalmente expressões de consulta SQL em seu código de linguagem orientado a objeto principal. Em contraste com o PL / SQL, que adiciona um paradigma imperativo ao SQL no lado do servidor de banco de dados, o LINQ traz o SQL para o nível do aplicativo. Graças a isso, as consultas em LINQ podem ser usadas para obter dados não apenas de bancos de dados relacionais, mas também de coleções de objetos, documentos XML e outras consultas LINQ.



A arquitetura do LINQ é bastante flexível e as definições de consulta estão profundamente integradas ao modelo OOP. LINQ permite que você crie seus próprios provedores para acessar novas fontes de dados. Você também pode definir sua própria maneira de executar a consulta e, por exemplo, converter a árvore de expressão LINQ da consulta em uma consulta para a fonte de dados desejada. Você pode usar expressões lambda e funções definidas no código do aplicativo no corpo da solicitação. É verdade que, no caso do LINQ to SQL, a consulta será executada no lado do servidor do banco de dados, onde essas funções não estarão disponíveis, mas os procedimentos armazenados podem ser usados ​​em seu lugar. A solicitação é a essência da linguagem de primeiro nível, você pode trabalhar com ela como com um objeto comum. O compilador é capaz de inferir automaticamente o tipo do resultado da consulta e gerar a classe apropriada, mesmo que não tenha sido declarada explicitamente.



Vamos tentar usar o LINQ para construir um modelo de domínio como um conjunto de consultas. Os fatos originais podem ser colocados em listas no lado do aplicativo ou em tabelas no lado do banco de dados, e os conceitos abstratos podem ser formatados como consultas LINQ. LINQ permite que você crie consultas com base em outras consultas, especificando-as na cláusula FROM. Isso permite que você construa um novo conceito baseado nos existentes.Os campos da seção SELECT corresponderão aos atributos do conceito. E a seção WHERE conterá dependências entre os conceitos. Um exemplo com faturas de uma publicação anterior será semelhante a este.



Colocaremos objetos com contas e informações de clientes nas listas:



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


E então vamos construir consultas para eles receberem contas não pagas e devedores:



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;


O modelo de domínio implementado com LINQ assumiu uma forma um tanto bizarra - vários estilos de programação estão envolvidos. O nível superior do modelo possui semântica imperativa. Pode ser representado como cadeias de transformações de objetos, construindo coleções de objetos sobre as coleções. Objetos de consulta são elementos do mundo OOP. Eles precisam ser criados, atribuídos a variáveis ​​e as referências a eles devem ser passadas para outras solicitações. No nível intermediário, o objeto de consulta implementa o procedimento para executar uma consulta, que é customizado funcionalmente com expressões lambda que permitem formar a estrutura de resultado na seção SELECT e filtrar registros na cláusula WHERE. O nível interno é representado pelo procedimento de execução da consulta, que possui semântica lógica e é baseado na álgebra relacional.



Embora LINQ tenha possibilitado a descrição do modelo de domínio, a sintaxe SQL visa principalmente buscar e manipular dados. Faltam algumas construções que seriam úteis na modelagem. Se em PL / SQL a estrutura de conceitos básicos era muito claramente representada na forma de tabelas e visualizações, então em LINQ ela acabou sendo renderizada em código OOP. Além disso, embora as tabelas e exibições possam ser referenciadas por nome, as consultas LINQ podem ser referenciadas em um estilo imperativo. Além disso, o SQL é limitado pelo modelo relacional e tem recursos limitados ao trabalhar com estruturas na forma de gráficos ou árvores.



Paralelos entre o modelo relacional e a programação lógica



Você pode notar que as implementações SQL e Prolog do modelo têm semelhanças. No SQL, construímos uma visão baseada em tabelas ou outras visões, e no Prolog construímos regras baseadas em fatos e regras. No SQL, as tabelas são uma coleção de campos e os predicados no Prolog são uma coleção de atributos. No SQL, especificamos dependências entre os campos da tabela como expressões na cláusula WHERE e no Prolog, usando predicados e variáveis ​​booleanas que vinculam atributos de predicado uns aos outros. Em ambos os casos, definimos declarativamente a especificação da solução e o mecanismo de execução de consulta embutido retorna os registros encontrados em SQL ou possíveis valores de variáveis ​​em Prolog para nós.



Essa semelhança não é acidental. Embora a base teórica do SQL - álgebra relacional tenha sido desenvolvida em paralelo com a programação lógica, mais tarde uma conexão teórica foi revelada entre eles. Eles têm uma base matemática comum - lógica de primeira ordem. O modelo de dados relacional descreve as regras para construir relacionamentos entre tabelas de dados, programação lógica - entre instruções. Ambas as teorias usam termos diferentes, são aplicadas em campos diferentes, foram desenvolvidas em paralelo, mas tinham uma base matemática comum.



A rigor, o cálculo relacional é uma adaptação da lógica de primeira ordem para trabalhar com dados tabulares. Esta questão é discutida com mais detalhes aqui.... Ou seja, qualquer expressão de álgebra relacional (qualquer consulta SQL) pode ser reformulada em uma expressão de lógica de primeira ordem e então implementada no Prolog. Mas não vice-versa. O cálculo relacional é um subconjunto da lógica de primeira ordem. Isso significa que, para alguns tipos de afirmações admissíveis na lógica de primeira ordem, não podemos encontrar analogias na álgebra relacional. Por exemplo, as capacidades de consultas recursivas em SQL são muito limitadas e a construção de relações transitivas também nem sempre está disponível. Operações de prólogo, como disjunção de destino e negação como recusa, são muito mais difíceis de implementar em SQL. A sintaxe flexível do Prolog oferece mais flexibilidade para trabalhar com estruturas aninhadas complexas e suporta operações de correspondência de padrões nelas.Isso o torna conveniente ao trabalhar com estruturas de dados complexas, como árvores e gráficos.



Mas você tem que pagar por tudo. Os algoritmos de consulta integrados em bancos de dados relacionais são mais simples e menos versáteis do que os algoritmos de inferência no Prolog. Isso torna possível otimizá-los e obter um desempenho muito superior. O Prolog também não é capaz de processar rapidamente milhões de linhas em bancos de dados relacionais. Além disso, o algoritmo de inferência do Prolog não garante o fim da execução do programa - a saída de algumas instruções pode levar à recursão infinita.



By the way, na intersecção de bancos de dados e programação lógica, também há tecnologia como bancos de dados dedutivos e a linguagem de regras e consultas a eles Datalog. Em vez de registros em tabelas, os bancos de dados dedutivos armazenam grandes quantidades de fatos e regras em um estilo lógico. E o Datalog se parece com o Prolog, mas se concentra em trabalhar com fatos combinados em conjuntos, não com fatos únicos. Além disso, alguns recursos da lógica de primeira ordem nele foram cortados a fim de otimizar o algoritmo de inferência para trabalho rápido com grandes quantidades de dados. Portanto, a sintaxe menos expressiva de uma linguagem lógica também tem suas vantagens.



Abordagem declarativa para a descrição da camada API



O SQL vincula a construção do modelo à camada de acesso a dados. Mas a programação declarativa também está se desenvolvendo ativamente na extremidade oposta do aplicativo - na camada API. Sua peculiaridade é que as informações sobre a estrutura das solicitações devem estar disponíveis para quem utiliza esta API. Ter uma descrição formal da estrutura de solicitações e respostas é uma boa forma. Consequentemente, existe um desejo de sincronizar esta descrição com o código do aplicativo, por exemplo, gerar classes de solicitação e resposta com base nela. Em que você precisará escrever a lógica para processar solicitações.



GraphQL é uma estrutura para a construção de APIs que vai muito além dessa abordagem tradicional e oferece não apenas uma linguagem de consulta, mas também um ambiente de execução de consulta. Não há necessidade de gerar código, o tempo de execução entende as descrições da solicitação de qualquer maneira. Para implementar a API usando GraphQL, você precisa:



  1. descrever os tipos de dados (objetos) do aplicativo que fazem parte das solicitações e respostas;
  2. descrever a estrutura de solicitações e respostas;
  3. implementar funções que implementam a lógica de criação de objetos para obter os valores de seus campos.


Tipos de dados são descrições de campos de objetos. Tipos como tipos escalares, listas, enumerações e referências a tipos aninhados são suportados. Como os campos de tipo podem conter referências a outros tipos, todo o esquema de dados pode ser representado como um gráfico. A solicitação é uma descrição da estrutura de dados solicitada da API. A descrição da solicitação inclui uma lista de objetos necessários, seus campos e atributos de entrada. Cada tipo de dados e cada um de seus campos deve ser associado a uma função de resolução. O resolvedor de tipo (objeto) descreve o algoritmo para obter seus objetos, o resolvedor de campo descreve os valores do campo do objeto. Eles representam funções em uma das linguagens funcionais ou orientadas a objetos. O tempo de execução GraphQL recebe uma solicitação, determina os tipos de dados necessários, chama seus resolvedores, incluindo ao longo de uma cadeia de objetos aninhados,coleta um objeto de resposta.



GraphQL combina a descrição do esquema de dados declarativos com algoritmos imperativos ou funcionais para obtê-los. O esquema de dados é descrito explicitamente e é fundamental para o aplicativo. Muitas pessoas apontam que é uma boa prática criar um esquema de dados que não duplique os esquemas da fonte de dados, mas esteja em conformidade com o modelo de domínio. Isso torna o GraphQL uma solução bastante popular para a integração de fontes de dados distintas.



Assim, a linguagem GraphQL permite expressar o modelo de domínio de uma maneira bastante clara, para distingui-lo do resto do código, para aproximar o modelo e sua implementação. Infelizmente, o componente declarativo da linguagem é limitado apenas à descrição da composição dos tipos de dados; todas as outras relações entre os elementos do modelo devem ser implementadas usando resolvedores. Por um lado, os resolvedores permitem que um desenvolvedor implemente de forma independente qualquer método de obtenção de dados para um objeto e qualquer relacionamento entre eles. Mas, por outro lado, você terá que tentar implementar opções de consulta mais complexas do que, por exemplo, acesso a um registro por chave. Por um lado, o esquema de dados em GraphQL mostra claramente a relação entre a camada de API e a camada de acesso a dados. Mas, por outro lado, a camada principal à qual o esquema de dados está vinculado é a camada API.O conteúdo do esquema de dados se ajusta a ele, não conterá entidades que não estejam envolvidas no processamento de solicitações. Embora o poder expressivo da linguagem de descrição de dados GraphQL seja inferior a linguagens declarativas completas como SQL e Prolog, a popularidade dessa estrutura mostra que as ferramentas para descrição de modelo declarativo podem e devem fazer parte das linguagens de programação modernas.



Vou resumir



PL / SQL é uma linguagem conveniente tanto para descrever um modelo de domínio na forma de tabelas e visualizações, quanto para a lógica de trabalhar com ele. Os componentes declarativos e procedimentais são totalmente integrados e complementares. O principal problema é que essa linguagem está intimamente ligada ao local de armazenamento de dados, ela só pode ser executada no lado do servidor de banco de dados e a lógica de execução da consulta é limitada ao modelo de dados relacional.



No lado do aplicativo, você pode usar tecnologias como LINQ e GraphQL para descrever o modelo em um formato declarativo. Usando o esquema de dados GraphQL, você pode descrever clara e muito claramente a estrutura do modelo de domínio, o aninhamento de seus conceitos. E o tempo de execução é capaz de coletar automaticamente os objetos necessários. Infelizmente, todos os outros relacionamentos e conexões entre conceitos, exceto para seu aninhamento, devem ser implementados na camada de funções de resolução. LINQ tem vantagens e desvantagens opostas. A sintaxe SQL flexível oferece mais flexibilidade para descrever as relações entre os conceitos. Mas fora da solicitação, termina a declaratividade, os objetos de solicitação são elementos do mundo OOP. Eles precisam ser criados, atribuídos a variáveis ​​e usados ​​em um estilo imperativo.



Eu gostaria de combinar as vantagens do LINQ e do GraphQL. Para que a descrição da estrutura dos conceitos fosse clara como no GraphQL, e as relações entre eles pudessem ser definidas com base na lógica como no SQL. E para que as definições de conceitos estejam disponíveis diretamente por nome como classes, sem a necessidade de criar explicitamente seus objetos, atribuí-los a variáveis, passar referências a elas, etc.



Vou começar a projetar tal solução desenvolvendo uma linguagem para descrever um modelo de domínio. Mas para isso é necessário fazer uma visão geral das linguagens de representação do conhecimento existentes. Portanto, na próxima publicação, quero falar sobre programação lógica, RDF, OWL e linguagens de lógica de quadro, compará-los e tentar encontrar recursos que seriam interessantes para a linguagem projetada para descrever lógica de negócios.



Para quem não deseja aguardar o lançamento de todas as publicações sobre o Habré, existe um texto completo em estilo científico em inglês, disponível no link: Hybrid Ontology-Oriented Programming for Semi-Structured Data Processing .



Links para publicações anteriores:

Projetando uma linguagem de programação multiparadigma. Parte 1 - Para que serve?



All Articles