Vou descrever brevemente a tarefa principal
Consiste em criar uma linguagem de programação que seja conveniente tanto para descrever o modelo de domínio quanto para trabalhar com ele. Tornar a descrição do modelo o mais natural possível, compreensível para o homem e próximo das especificações do software. Mas, ao mesmo tempo, deve fazer parte do código em uma linguagem de programação completa. Para isso, o modelo terá a forma de uma ontologia e será composto por fatos concretos, conceitos abstratos e relações entre eles. Os fatos descreverão o conhecimento direto da área de assunto e os conceitos e relações lógicas entre eles - sua estrutura.
Além das ferramentas de modelagem, a linguagem também precisará de ferramentas para preparar os dados iniciais do modelo, criando dinamicamente seus elementos, processando os resultados das consultas a ele, criando aqueles elementos do modelo que são mais convenientes para descrever em forma algorítmica. Tudo isso é muito mais conveniente de fazer descrevendo explicitamente a seqüência de cálculos. Por exemplo, usando OOP ou uma abordagem funcional.
E, claro, ambas as partes da linguagem devem interagir intimamente e se complementar. Para que possam ser facilmente combinados em uma aplicação e resolver cada tipo de problema com a ferramenta mais conveniente.
Vou começar minha história perguntando por que criar tal linguagem, por que uma linguagem híbrida e onde ela seria útil. Nos próximos artigos, pretendo dar uma breve visão geral das tecnologias e estruturas que permitem combinar um estilo declarativo com um imperativo ou funcional. Além disso, será possível revisar as linguagens de descrição de ontologias, formular os requisitos e princípios básicos de uma nova linguagem híbrida e, em primeiro lugar, seu componente declarativo. Finalmente, descreva seus conceitos e elementos básicos. Depois disso, consideraremos quais problemas surgem ao usar os paradigmas declarativo e imperativo juntos e como eles podem ser resolvidos. Também analisaremos algumas questões de implementação de linguagem, por exemplo, o algoritmo de inferência. Finalmente, vamos dar uma olhada em um dos exemplos de sua aplicação.
Escolher o estilo de linguagem de programação correto é uma condição importante para a qualidade do código
Muitos de nós tiveram que lidar com o apoio a projetos complexos criados por outras pessoas. É bom se a equipe tiver pessoas familiarizadas com o código do projeto e puderem explicar como funciona, houver documentação, o código estiver limpo e compreensível. Mas, na realidade, muitas vezes acontece de outra maneira - os autores do código desistem muito antes de você chegar a este projeto, não há documentação alguma, ou é muito fragmentada e desatualizada há muito tempo, e sobre a lógica de negócios do componente necessário, um analista de negócios ou um projeto - o gerente pode dizer apenas em termos gerais. Nesse caso, a limpeza e a compreensão do código são críticas.
A qualidade do código tem muitos aspectos, um deles é a escolha correta da linguagem de programação, que deve corresponder ao problema a ser resolvido. Quanto mais fácil e natural um desenvolvedor implementar suas ideias no código, mais rápido ele pode resolver o problema e menos erros cometerá. Agora temos um número bastante grande de paradigmas de programação para escolher, cada um com sua própria área de aplicação. Por exemplo, a programação funcional é preferível para aplicativos com foco computacional porque fornece mais flexibilidade para estruturar, combinar e reutilizar funções que executam operações nos dados. Programação Orientada a Objetossimplifica a criação de estruturas de dados e funções por meio de encapsulamento, herança, polimorfismo. OOP é adequado para aplicativos orientados a dados. A programação lógica é conveniente para problemas baseados em regras que requerem trabalhar com tipos de dados complexos e recursivamente definidos, como árvores e gráficos, e é adequada para resolver problemas combinatórios. Além disso, a programação reativa, orientada por eventos e de vários agentes tem seus escopos.
Linguagens de programação modernas de uso geral podem oferecer suporte a vários paradigmas. Combinar os paradigmas funcional e OOP tornou-se a tendência por muito tempo.
A programação de lógica funcional híbrida também tem uma longa história, mas nunca foi além do mundo acadêmico. O mínimo de atenção é dado à combinação de programação OOP lógica e imperativa (pretendo falar sobre eles com mais detalhes em uma das próximas publicações). Embora, na minha opinião, uma abordagem lógica possa ser muito útil no campo tradicional de OOP - aplicações de servidor de sistemas de informação corporativa. Você só precisa olhar para isso de um ângulo ligeiramente diferente.
Por que acho o estilo de programação declarativo subestimado
Vou tentar fundamentar meu ponto de vista.
Para fazer isso, considere o que pode ser uma solução de software. Seus principais componentes são: a parte cliente (desktop, mobile, aplicações web); lado do servidor (um conjunto de serviços separados, microsserviços ou um aplicativo monolítico); sistemas de gerenciamento de dados (relacionais, orientados a documentos, orientados a objetos, bancos de dados gráficos, serviços de cache, índices de pesquisa). Uma solução de software precisa interagir com mais do que apenas pessoas - usuários. A integração com serviços externos que fornecem informações via API é uma tarefa comum. Além disso, as fontes de dados podem ser documentos de áudio e vídeo, textos em linguagem natural, conteúdo de páginas da web, registros de eventos, dados médicos, leituras de sensores, etc.
Por um lado, um aplicativo de servidor armazena dados em um ou mais bancos de dados. Por outro lado, ele responde a solicitações provenientes de terminais de API, processa mensagens de entrada e responde a eventos. As estruturas de mensagens e consultas quase nunca correspondem às estruturas armazenadas em bancos de dados. Os formatos de dados de entrada / saída são projetados para uso externo, otimizados para o consumidor dessas informações e ocultam a complexidade da aplicação. Os formatos de dados armazenados são otimizados para seu sistema de armazenamento, por exemplo, para um modelo de dados relacional. Portanto, precisamos de alguma camada intermediária de conceitos que permitirá combinar a entrada / saída do aplicativo com sistemas de armazenamento de dados. Normalmente, essa camada de middleware é chamada de camada de lógica de negócios e implementa as regras e princípios de comportamento para objetos no domínio.
A tarefa de vincular o conteúdo do banco de dados aos objetos do aplicativo também não é fácil. Se a estrutura das tabelas no armazenamento corresponder à estrutura dos conceitos no nível do aplicativo, você poderá usar a tecnologia ORM. Mas para casos mais complexos do que o acesso a registros por chave primária e operações CRUD, você deve alocar uma camada separada de lógica para trabalhar com o banco de dados. Normalmente, o esquema do banco de dados é o mais geral possível, para que diferentes serviços possam trabalhar com ele. Cada um dos quais mapeia este esquema de dados para seu próprio modelo de objeto. A estrutura da aplicação fica ainda mais confusa se a aplicação não funcionar com um armazenamento de dados, mas com vários, de diferentes tipos, carregando dados de fontes de terceiros, por exemplo, através da API de outros serviços.Nesse caso, é necessário criar um modelo de domínio unificado e mapear dados de diferentes fontes para ele.
Em alguns casos, o modelo de domínio pode ter uma estrutura complexa de vários níveis. Por exemplo, ao compilar relatórios analíticos, alguns indicadores podem ser construídos a partir de outros, que, por sua vez, serão uma fonte para a construção do terceiro, etc. Além disso, os dados de entrada podem ter uma forma semiestruturada. Esses dados não possuem um esquema estrito, como, por exemplo, no modelo de dados relacional, mas ainda contêm algum tipo de marcação que permite extrair informações úteis deles. Exemplos de tais dados podem ser recursos da Web Semântica, resultados de análise de página da Web, documentos, logs de eventos, leituras de sensores, resultados de pré-processamento de dados não estruturados, como textos, vídeos e imagens, etc. O esquema de dados dessas fontes será criado exclusivamente no nível do aplicativo. Haverá também um códigoconverter os dados de origem em objetos de lógica de negócios.
Portanto, o aplicativo contém não apenas algoritmos e cálculos, mas também uma grande quantidade de informações sobre a estrutura do modelo de domínio - a estrutura de seus conceitos, seus relacionamentos, hierarquia, regras para construir alguns conceitos com base em outros, regras para converter conceitos entre diferentes camadas do aplicativo, etc. Quando elaboramos uma documentação ou um projeto, descrevemos essas informações declarativamente - na forma de estruturas, diagramas, declarações, definições, regras, descrições em linguagem natural. É conveniente para nós pensarmos desta forma. Infelizmente, nem sempre é possível expressar essas descrições da mesma maneira natural no código.
Vamos considerar um pequeno exemplo e especular como será sua implementação usando diferentes paradigmas de programação
Digamos que temos 2 arquivos CSV. No primeiro arquivo:
A primeira coluna contém o ID do cliente.
O segundo contém a data.
No terceiro - o valor faturado,
no quarto - o valor do pagamento.
No segundo arquivo:
A primeira coluna armazena o ID do cliente.
No segundo - o nome.
O terceiro é o endereço de e-mail.
Vamos introduzir algumas definições:
A fatura inclui o identificador do cliente, a data, o valor faturado, o valor do pagamento e a dívida das células de uma linha do arquivo 1.
O valor da dívida é a diferença entre o valor faturado e o valor do pagamento.
O cliente é descrito usando o ID do cliente, nome e endereço de e-mail das células de uma linha no arquivo 2.
Uma fatura não paga é uma fatura de dívida positiva.
As contas são vinculadas a um cliente pelo valor de ID do cliente.
Devedor é um cliente que tem pelo menos uma fatura não paga, cuja data é 1 mês anterior à data atual.
Um inadimplente malicioso é um cliente que tem mais de 3 faturas não pagas.
Além disso, usando essas definições, você pode implementar a lógica de enviar um lembrete a todos os devedores, transmitir dados sobre inadimplentes persistentes aos cobradores, calcular uma penalidade sobre o valor da dívida, compilar vários relatórios, etc.
Em linguagens de programação funcionalessa lógica de negócios é implementada usando um conjunto de estruturas de dados e funções para transformá-los. Além disso, as estruturas de dados são fundamentalmente separadas das funções. Como resultado, o modelo, e especialmente seu componente, como relações entre entidades, fica oculto dentro de um conjunto de funções, espalhado pelo código do programa. Isso cria uma grande lacuna entre a descrição declarativa do modelo e sua implementação de software e complica seu entendimento. Principalmente se o modelo tiver um grande volume.
Estruturação de um programa orientado a objetosestilo ajuda a mitigar esse problema. Cada entidade de domínio é representada por um objeto cujos campos de dados correspondem aos atributos da entidade. E os relacionamentos entre entidades são implementados na forma de relacionamentos entre objetos, em parte com base nos princípios de OOP - herança, abstração de dados e polimorfismo, em parte - usando padrões de design. Mas, na maioria dos casos, os relacionamentos devem ser implementados codificando-os em métodos de objeto. Além disso, além de criar classes que representam entidades, você também precisará de estruturas de dados para ordená-las, algoritmos para preencher essas estruturas e buscar informações nelas.
No exemplo com devedores, podemos descrever classes que descrevem a estrutura dos conceitos "Conta" e "Cliente". Mas a lógica de criar objetos, vinculando objetos de contas e clientes uns aos outros, é frequentemente implementada separadamente em classes ou métodos de fábrica. Para os conceitos de devedores e faturas não pagas, classes separadas não são necessárias, seus objetos podem ser obtidos filtrando clientes e faturas onde eles são necessários. Como resultado, alguns dos conceitos do modelo serão implementados na forma de classes explicitamente, alguns - implicitamente, no nível do objeto. Alguns dos relacionamentos entre os conceitos estão nos métodos das classes correspondentes e alguns são separados. A implementação do modelo será espalhada por classes e métodos, misturados com a lógica auxiliar de seu armazenamento, pesquisa, processamento e conversão de formato. Levará algum esforço para encontrar esse modelo em seu código e entendê-lo.
O mais próximo da descrição será a implementação do modelo conceitual em linguagens de representação do conhecimento . Exemplos de tais linguagens são Prolog, Datalog, OWL, Flora e outros. Pretendo falar sobre esses idiomas na terceira publicação. Eles são baseados na lógica de primeira ordem ou seus fragmentos, por exemplo, lógica descritiva. Essas linguagens permitem, de forma declarativa, especificar a especificação da solução do problema, descrever a estrutura do objeto ou fenômeno modelado e o resultado esperado. E os motores de busca integrados encontrarão automaticamente uma solução que atenda às condições especificadas. A implementação do modelo de domínio em tais linguagens será extremamente concisa, compreensível e próxima da descrição em linguagem natural.
Por exemplo, a implementação do problema com devedores no Prolog será muito próxima das definições do exemplo. Para fazer isso, as células da tabela precisarão ser representadas como fatos e as definições do exemplo deverão ser apresentadas como regras. Para comparar contas e clientes, basta especificar a relação entre eles na regra, e seus valores específicos serão exibidos automaticamente.
Primeiro, declaramos fatos com o conteúdo das tabelas no formato: ID da tabela, linha, coluna, valor:
cell(“Table1”,1,1,”John”).
Em seguida, daremos nomes a cada uma das colunas:
clientId(Row, Value) :- cell(“Table1”, Row, 1, Value).
Então você pode combinar todas as colunas em um conceito:
bill(Row, ClientId, Date, AmountToPay, AmountPaid) :- clientId(Row, ClientId), date(Row, Date), amountToPay(Row, AmountToPay), amountPaid(Row, AmountPaid).
unpaidBill(Row, ClientId, Date, AmountToPay, AmountPaid) :- bill(Row, ClientId, Date, AmountToPay, AmountPaid), AmountToPay > AmountPaid.
debtor(ClientId, Name, Email) :- client(ClientId, Name, Email), unpaidBill(_, ClientId, _, _, _).
Etc.
As dificuldades começarão ao trabalhar com o modelo: ao implementar a lógica de envio de mensagens, transferência de dados para outros serviços, cálculos algorítmicos complexos. O ponto fraco do Prolog é sua descrição de sequências de ações. Sua implementação declarativa, mesmo em casos simples, pode parecer muito pouco natural e requer esforço e habilidade significativos. Além disso, a sintaxe do Prolog não é muito próxima do modelo orientado a objetos, e as descrições de conceitos compostos complexos com um grande número de atributos serão bastante difíceis de entender.
Como podemos reconciliar a linguagem de desenvolvimento funcional ou orientada a objetos mainstream com a natureza declarativa do modelo de domínio?
A abordagem mais conhecida é o design orientado a objetos (Domain-Driven Design). Esta metodologia facilita a criação e implementação de modelos de domínio complexos. Ele determina que todos os conceitos de modelo sejam expressos em código explicitamente na camada de lógica de negócios. Os conceitos do modelo e os elementos do programa que os implementam devem ser o mais próximos possível uns dos outros e corresponder a uma única linguagem, compreensível para programadores e especialistas no assunto.
Um modelo de domínio rico para o exemplo com devedores conterá adicionalmente classes para os conceitos "Fatura não paga" e "Devedor", classes agregadas para combinar os conceitos de contas e clientes, fábricas para criar objetos. A implementação e o suporte de tal modelo consomem mais tempo e o código é complicado - o que poderia ser feito anteriormente em uma linha requer várias classes em um modelo avançado. Como resultado, na prática, essa abordagem só faz sentido quando grandes equipes estão trabalhando em modelos de escala complexos.
Em alguns casos, a solução pode ser uma combinação de uma linguagem de programação funcional básica ou orientada a objetos e um sistema de representação de conhecimento externo.... O modelo de domínio pode ser transferido para uma base de conhecimento externa, por exemplo, em Prolog ou OWL, e o resultado das consultas a ele é processado no nível do aplicativo. Mas esta abordagem complica a solução, as mesmas entidades devem ser implementadas nas duas linguagens, a interação entre elas deve ser configurada via API, adicionalmente suportada pelo sistema de representação de conhecimento, etc. Portanto, só se justifica se o modelo for grande e complexo, exigindo inferência lógica. Isso será um exagero para a maioria das tarefas. Além disso, esse modelo nem sempre pode ser desconectado sem dor do aplicativo.
Outra opção para combinar bases de conhecimento e aplicativos OOP é a programação orientada a ontologia.... Esta abordagem é baseada nas semelhanças entre as ferramentas de descrição de ontologias e o modelo de programação de objetos. Classes, entidades e atributos de ontologia escritos, por exemplo, na linguagem OWL, podem ser mapeados automaticamente para classes, objetos e seus campos do modelo de objeto. E então as classes resultantes podem ser usadas junto com outras classes do aplicativo. Infelizmente, a implementação básica dessa ideia terá um escopo bastante limitado. As linguagens de ontologia são bastante expressivas e nem todos os componentes da ontologia podem ser convertidos em classes OOP de forma simples e natural. Além disso, para implementar a inferência completa, não é suficiente apenas criar um conjunto de classes e objetos. Ele precisa de informações sobre os elementos da ontologia de forma explícita, por exemplo, na forma de metaclasses.Pretendo falar sobre essa abordagem com mais detalhes em uma das publicações a seguir.
Também existe uma abordagem extrema para o desenvolvimento de software , como o desenvolvimento dirigido por modelo . Segundo ele, a principal tarefa do desenvolvimento passa a ser a criação de modelos de domínio, a partir dos quais o código do programa é gerado automaticamente. Mas, na prática, essa solução radical nem sempre é flexível o suficiente, especialmente em termos de desempenho do programa. O criador de tais modelos deve combinar as funções de programador e analista de negócios. Portanto, essa abordagem não poderia impedir as abordagens tradicionais de implementação do modelo em linguagens de programação de uso geral.
Todas essas abordagens são bastante complicadas e fazem sentido para modelos de alta complexidade, muitas vezes descritos separadamente da lógica de seu uso. Gostaria de algo mais leve, confortável e natural. Assim, com o auxílio de uma linguagem foi possível descrever tanto o modelo de forma declarativa quanto os algoritmos para seu uso. Portanto, pensei em como combinar o paradigma orientado a objetos ou funcional (vamos chamá-lo de componente de computação ) e o paradigma declarativo (vamos chamá-lo de componente de modelagem ) em uma única linguagem de programação híbrida . À primeira vista, esses paradigmas parecem opostos entre si, mas é ainda mais interessante tentar.
Portanto, o objetivo é criar uma linguagem que seja confortável para a modelagem conceitual baseada em dados semiestruturados e díspares. A forma do modelo deve ser próxima à ontologia e consistir em uma descrição das entidades do domínio e as relações entre elas. Ambos os componentes da linguagem devem ser integrados, inclusive no nível semântico.
Os elementos da ontologia devem ser entidades da linguagem de primeiro nível - eles podem ser passados para funções como argumentos, atribuídos a variáveis, etc. Uma vez que a ontologia-modelo se tornará um dos principais elementos do programa, essa abordagem de programação pode ser chamada de orientada ontologicamente. Combinar a descrição do modelo com os algoritmos para seu uso tornaria o código do programa mais compreensível e natural para humanos, o aproximaria do modelo conceitual do domínio e simplificaria o desenvolvimento e manutenção de software.
Chega pela primeira vez. No próximo post, quero falar sobre algumas tecnologias modernas que combinam estilos imperativos e declarativos - PL / SQL, Microsoft LINQ e GraphQL. 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 .