O componente de modelagem de linguagem híbrida é um conjunto de conceitos-objetos conectados por relacionamentos lógicos. Consegui falar sobre as principais formas de definir conceitos, incluindo herança e definir relações entre eles. E também sobre algumas das nuances da programação lógica, incluindo a semântica do operador de negação e a lógica de ordem superior. Uma lista completa de publicações sobre este assunto pode ser encontrada no final deste artigo.
No campo do trabalho com dados, o líder indiscutível é a linguagem SQL. Alguns de seus recursos, que se mostraram muito convenientes na prática, como a agregação, posteriormente migraram para a programação lógica. Portanto, será útil tomar emprestado do SQL o máximo possível para o componente de modelagem. Neste artigo, quero mostrar como consultas aninhadas, junções externas e agregação podem ser incorporadas em definições de conceito. Também falarei sobre outro tipo de conceito, que é descrito por meio de uma função que gera objetos (entidades) em um estilo algorítmico sem recorrer à busca lógica. E vou mostrar como você pode usar matrizes de objetos como conceitos-pai por analogia com a operação SQL UNNESTque converte coleções em formato de tabela e permite que sejam unidas a outras tabelas na cláusula FROM .
Definições anônimas de conceitos
No mundo SQL, as consultas aninhadas são uma ferramenta frequentemente usada quando é necessário obter dados intermediários para processamento posterior na consulta principal. O componente de modelagem não tem uma necessidade tão urgente de dados intermediários, uma vez que a forma de obtê-los pode ser formalizada como um conceito separado. Mas há casos em que definições de conceitos aninhados seriam úteis.
Às vezes, você precisa modificar um pouco o conceito, selecionar seus atributos individuais, filtrar os valores. Se essa modificação for necessária apenas em um lugar, não faz sentido criar um conceito separado com um nome exclusivo. Esta situação é frequentemente encontrada quando conceitos são argumentos para funções como existe , encontra ou findOne , que verifica a dedutibilidade do conceito, encontrando todo ou apenas o primeiro objeto (entidade) do conceito. Aqui você pode fazer uma analogia com funções anônimas em linguagens de programação funcionais, que são freqüentemente usadas como argumentos para funções como mapear , localizar , filtrar , etc.
Considere a sintaxe para definir um conceito anônimo. Em geral, segue a sintaxe de uma definição de conceito comum, exceto que, em alguns casos, você pode omitir as listas de atributos e o nome do conceito filho. Se um conceito anônimo for usado como argumento para existe, então seu nome e a lista de atributos não são importantes, basta verificar se há pelo menos algum resultado. As funções find e findOne podem não precisar do nome do conceito se a saída não for usada como um conceito completo, mas apenas como um conjunto de atributos em uma matriz associativa. Se nenhum atributo for especificado, o mecanismo de herança será usado por padrão e os atributos serão herdados dos conceitos pai. Se nenhum nome de conceito for especificado, ele será gerado automaticamente.
Vamos tentar explicar o que foi escrito acima usando alguns exemplos. Usando a função exists , você pode verificar a dedutibilidade ou não dedutibilidade de um conceito incorporado:
concept freeExecutor is executor e where not exists ( task t where t.executor = e.id and t.status in ('assigned', 'in process') )
Neste exemplo, o conceito anônimo:
(task t where t.executor = e.id and t.status in ('assigned', 'in process'))
é na verdade um conceito que herda todos os atributos do conceito de tarefa :
(concept _unanimous_task_1 as task t where t.executor = e.id and t.status in ('assigned', 'in process'))
A função find permite que você retorne como uma lista todos os valores de um conceito, que podem então ser associados a um atributo:
concept customerOrdersThisYear is customer c with orders where c.orders = find( (id = o.id, status = o.status, createdDate = o.createdDate, total = o.total) from order o where o.customerId = c.id and o.createdDate > '2021-01-01' )
Neste exemplo, estendemos a noção de cliente com uma lista de pedidos, que são objetos que contêm os atributos selecionados da noção de pedido . Especificamos uma lista de atributos para um conceito anônimo, mas seu nome foi omitido.
Condições na seção onde conceitos anônimas podem incluir outros atributos dos conceitos mães ou filiais, neste caso c.id . Uma característica dos conceitos anônimos é que todas essas variáveis externas e atributos devem necessariamente estar associados a valores no momento de iniciar a busca por soluções. Assim, os objetos do conceito anônimo podem ser encontrados somente após encontrar os objetos do conceito de cliente . ...
Conexões externas
Definições de conceitos anônimos também pode ser usado na da seção , onde eles representam conceitos principais. Além disso, na definição de um conceito anônimo, é possível transferir algumas das condições que o ligam a outros conceitos, o que terá um efeito especial. Essas condições serão verificadas na fase de encontrar uma solução para o conceito anônimo e não afetarão o processo de inferência do conceito filho. Aqui você pode fazer uma analogia entre as condições na seção where de um conceito anônimo e as condições na seção JOIN ON do SQL.
Assim, os conceitos anônimos podem ser usados para implementar um análogo SQL da junção externa esquerda. Isso requer três coisas.
- Primeiro, substitua o conceito de pai desejado por um conceito anônimo baseado nele e transfira todas as conexões com outros conceitos de pai para ele.
- Em segundo lugar, para apontar que o fracasso da inferência desse conceito não deve levar a um fracasso automático da inferência de todo o conceito de criança. Para fazer isso, você precisa marcar este conceito pai com a palavra-chave opcional .
- E em terceiro lugar, na seção onde do conceito de filho, você pode verificar se há soluções para esse conceito anônimo.
Vejamos um pequeno exemplo:
concept taskAssignedTo (task = t, assignee = u, assigneeName) from task t, optional (user where id = t.assignedTo) u where assigneeName = if(defined(u), u.firstName + ' ' + u.lastName, 'Unassigned')
Os atributos do conceito taskAssignedTo incluem objetos da tarefa, seu executor e separadamente o nome do executor. Os conceitos pai são tarefa e usuário , e o último pode estar vazio se a tarefa ainda não tiver um executor. Ele está envolvido em uma definição de conceito anônima, precedida pela palavra-chave opcional . O procedimento de inferência irá primeiro encontrar objetos da tarefa conceito , então, com base no usuário, ele irá criar um conceito anônima, associando-a com um valor específico do AssignedTo atributo da tarefa conceito . Palavra-chave opcional diz à rotina de inferência que se o conceito falhar, seu objeto será associado ao valor especial UNDEFINED . E verificar o resultado de sua saída no nível de conceito filho permite que o atributo assigneeName defina um valor padrão. Se a palavra-chave opcional não foi especificada, a falha em deduzir um conceito anônimo resultaria na falha da ramificação atual da pesquisa de conceito filho. Isso seria análogo à junção interna do SQL.
Porque as condições no onde cláusula do conceito anônimo incluem o AssignedTo atributo do outro conceito pai tarefa , então a busca por objetos do usuário conceito só é possível depois de vincular objetos de tarefa com valores. Eles não podem ser trocados:
from optional (user where id = t.assignedTo) u, task t
Como no estágio inicial o valor de t.assignedTo será desconhecido, não funcionará para criar uma definição de um conceito anônimo.
Se em SQL a ordem das tabelas na da secção não importa, então, no Prolog, a ordem dos predicados em uma regra determina exclusivamente a sequência de passagem pela árvore de decisão. O mesmo pode ser dito para o componente de simulação, cuja regra de saída é baseada na resolução SLD utilizada no Prolog. Nele, o resultado da saída de objetos do conceito esquerdo determina as restrições para a saída de objetos do direito. Por causa disso, infelizmente, não funcionará implementar as operações de junção externa direita e junção externa completa da mesma maneira natural. Como a cardinalidade do conjunto de resultados do conceito pai direito pode ser maior do que a do esquerdo, eles precisarão produzir na direção oposta - do conceito direito para a esquerda. Infelizmente, as peculiaridades do procedimento de inferência escolhido impõem suas limitações à funcionalidade da linguagem. Mas a operação de junção externa completa pode ser emulada juntando-se a interna,sindicatos de esquerda e direita:
concept outerJoinRelation( concept1Name, concept2Name, concept1Key, concept2Key, concept1 = c1, concept2 = c2 ) from <concept1Name> c1, <concept2Name> c2 where c1.<concept1Key> = c2.<concept2Key>; concept outerJoinRelation( concept1Name, concept2Name, concept1Key, concept2Key, concept1 = c1, concept2 = null ) from <concept1Name> c1 where not exists( <concept2Name> c2 where c1.<concept1Key> = c2.<concept2Key>); concept outerJoinRelation( concept1Name, concept2Name, concept1Key, concept2Key, concept1 = null, concept2 = c2 ) from <concept2Name> c2 where not exists( <concept1Name> c1 where c1.<concept1Key> = c2.<concept2Key>);
Este conceito generalizado é descrito usando a lógica de ordem superior descrita no artigo anterior . Sua definição está dividida em três partes. O primeiro encontrará intersecções de conceitos, o segundo - objetos que o conceito de esquerda possui e o de esquerda não, e o terceiro - vice-versa. Como os nomes de cada parte são iguais, os resultados de suas inferências serão combinados.
Agregação
A agregação é parte integrante da álgebra relacional e da programação lógica. No SQL, a cláusula GROUP BY permite agrupar linhas que possuem o mesmo valor-chave em linhas de resumo. Ele permite que você remova valores duplicados e é comumente usado com funções agregadas, como soma , contagem , mínimo , máximo , médio.... Para cada grupo de linhas, as funções de agregação retornam um valor comum com base em todas as linhas desse grupo. Na programação lógica, a agregação tem uma semântica mais complexa. Isso se deve ao fato de que em alguns casos de definição recursiva de regras SLD, a resolução entra em um loop infinito e não consegue se completar. Como no caso de negação como falha, o problema de recursão na operação de agregação é resolvido usando semântica de modelo persistente ou semântica bem fundamentada. Tentei falar brevemente sobre essas abordagens no artigo anterior . Mas, como a semântica do componente de modelagem deve ser a mais simples possível, a resolução SLD padrão é preferida. E o problema de evitar a recursão infinita é melhor resolvido reformando as conexões entre os conceitos.
A agregação pode naturalmente ser implementada em um estilo funcional usando o componente de computação de uma linguagem híbrida. Para fazer isso, uma função que reduz os resultados da inferência em grupos únicos e calcula as funções de agregação para cada um deles é suficiente. Mas dividir a definição de um conceito em partes lógicas e funcionais não será a solução mais conveniente para uma ferramenta tão importante como a agregação. Melhor expandir a sintaxe da definição para incluir uma seção de agrupamento e funções de agregação:
concept < > < > ( < > = <>, ... ) group by < >, ... from < > < > ( < > = <> , ... ), ... where < >
O agrupamento por seção , assim como no SQL, contém uma lista de atributos pelos quais o agrupamento é realizado. As expressões de relacionamento também podem incluir funções de agregação. Expressões contendo tais funções serão consideradas indefinidas até que os valores de todos os conceitos pais sejam encontrados e o agrupamento seja executado. Em seguida, seus valores podem ser calculados para cada grupo, associados a atributos e / ou usados para filtrar grupos. Com essa abordagem preguiçosa para avaliar e verificar as condições, não há necessidade de uma seção HAVING separando as condições do filtro antes e depois do agrupamento. O tempo de execução fará isso automaticamente.
As principais funções de agregação são contagem , soma, méd. , mín. , máx . A finalidade das funções pode ser entendida a partir de seus nomes. Como o componente de modelagem pode trabalhar naturalmente com tipos de dados compostos, você também pode adicionar uma função que retorna valores agrupados como uma lista. Vamos chamá-lo de grupo . Seu argumento de entrada é uma expressão. A função retorna uma lista dos resultados da avaliação desta expressão para cada elemento do grupo. Ao combiná-lo com outras funções, você pode implementar qualquer função de agregação arbitrária. O grupo função será mais conveniente do que tais funções SQL como group_concat ou json_arrayagque geralmente são usados como uma etapa intermediária para obter uma matriz de valores de campo.
Exemplo de agrupamento:
concept totalOrders ( customer = c, orders = group(o), ordersTotal = sum(o.total) ) group by customer from customer c, order o where c.id = o.customerId and ordersTotal > 100
O atributo de pedidos conterá uma lista de todos os pedidos do usuário, ordersTotal - o total de todos os pedidos. A condição ordersTotal> 100 será verificada após o agrupamento ser feito e a função de soma ser calculada .
Conceito definido por função
A forma lógica declarativa de descrever conceitos nem sempre é conveniente. Às vezes, será mais conveniente definir uma sequência de cálculos, cujo resultado será a essência do conceito. Essa situação geralmente surge quando é necessário carregar fatos de fontes de dados externas, por exemplo, de um banco de dados, arquivos, enviar solicitações para serviços externos, etc. Será conveniente representar o conceito na forma de uma função que traduz a consulta de entrada em uma consulta para o banco de dados e retorna o resultado da execução dessa consulta. Às vezes, faz sentido abandonar uma conclusão lógica, substituindo-a por uma implementação específica que leva em consideração as especificidades de um problema específico e o resolve com mais eficiência. Também é mais conveniente descrever em um estilo funcional sequências infinitas que geram entidades de conceito, por exemplo, uma sequência de inteiros.
Os princípios de trabalho com tais conceitos devem ser os mesmos que os demais conceitos descritos acima. A busca por soluções deve ser iniciada com os mesmos métodos. Eles próprios podem ser usados como conceitos pais nas definições de conceitos. Apenas a implementação interna da busca por uma solução deve ser diferente. Portanto, apresentaremos outra maneira de definir um conceito usando uma função:
concept < > ( < >, ... ) by < >
Para definir um conceito definido por meio de uma função, você deve especificar uma lista de seus atributos e uma função para gerar objetos. Decidi fazer da lista de atributos um elemento obrigatório da definição, pois isso simplificará o uso de tal conceito - para entender sua estrutura, você não terá que estudar a função de gerar objetos.
Agora vamos falar sobre a função de geração de objetos. Obviamente, ele deve receber uma solicitação como entrada - os valores iniciais dos atributos. Como esses valores podem ser especificados ou não, por conveniência, eles podem ser colocados em uma matriz associativa, que será o argumento de entrada da função. Também será útil saber em que modo a busca pelos significados dos conceitos é iniciada - encontre todos os valores possíveis, encontre apenas o primeiro ou apenas verifique a existência de uma solução. Portanto, adicionamos o modo de pesquisa como o segundo argumento de entrada.
O resultado da avaliação da função deve ser uma lista de objetos de conceito. Mas, como o procedimento de inferência baseado em pesquisa com backtracking consumirá esses valores um de cada vez, é possível fazer com que o argumento de saída da função não seja a própria lista, mas um iterador para ela. Isso tornaria a definição do conceito mais flexível, por exemplo, permitiria, se necessário, implementar uma avaliação preguiçosa ou uma sequência infinita de objetos. Você pode usar um iterador de qualquer coleção padrão ou criar sua própria implementação customizada. O elemento da coleção deve ser uma matriz associativa com os valores dos atributos do conceito. As essências do conceito serão criadas com base automaticamente.
Usar um iterador como o tipo de retorno tem suas desvantagens. É mais complicado e menos amigável do que simplesmente retornar uma lista de resultados. Encontrar a melhor opção que alie versatilidade, simplicidade e usabilidade é um desafio para o futuro.
Como exemplo, considere o conceito que descreve os intervalos de tempo. Digamos que queremos dividir o dia de trabalho em intervalos de 15 minutos. Podemos fazer isso com uma função bastante simples:
concept timeSlot15min (id, hour, minute) by function(query, mode) { var timeSlots = []; var curId = 1; for(var curHour = 8; curHour < 19; curHour += 1) { for(var curMinute = 0; curMinute < 60; curMinute += 15) { timeSlots.push({ id: curId, hour: curHour, minute: curMinute; }); curId++; } } return timeSlots.iterator(); }
A função retorna um iterador para todos os valores possíveis de um intervalo de tempo de 15 minutos para um dia útil. Pode ser usado, por exemplo, para pesquisar slots livres que ainda não foram reservados:
concept freeTimeSlot is timeSlot15min s where not exists (bookedSlot b where b.id = s.id)
A função não verifica o resultado dos cálculos quanto à conformidade com a consulta da consulta , isso será feito automaticamente ao converter um array de atributos em uma entidade. Mas, se necessário, os campos de consulta podem ser usados para otimizar a função. Por exemplo, forme uma consulta de banco de dados com base nos campos de consulta de um conceito.
Um conceito por meio de uma função combina semântica lógica e funcional. Se no paradigma funcional a função calcula o resultado para os valores dados dos argumentos de entrada, então no paradigma lógico não há divisão em argumentos de saída e entrada. Apenas parte dos argumentos podem ser fornecidos, e em qualquer combinação, e a função precisa encontrar os valores dos argumentos restantes. Na prática, nem sempre é possível implementar uma função capaz de realizar cálculos em qualquer direção, por isso faz sentido limitar as combinações possíveis de argumentos livres. Por exemplo, declare que alguns argumentos devem ser associados a valores antes de avaliar uma função. Para fazer isso, marque tais atributos na definição do conceito com a palavra-chave necessária .
Como exemplo, considere o conceito que define os valores de uma determinada escala exponencial.
concept expScale (value, position, required limit) by function(query, mode) { return { _curPos = 0, _curValue = 1, next: function() { if(!this.hasNext()) { return null; } var curItem = {value: this._curValue, position: this._curPosition, limit: query.limit}; this._curPos += 1; this._curValue = this._curValue * Math.E; return curItem; }, hasNext: function() { return query.limit == 0 || this._curPos < query.limit; } }}
A função retorna um iterador que gera entidades de conceito usando avaliação lenta. O tamanho da sequência é limitado pelo valor do atributo limit , mas se for zero, torna-se infinito. Conceitos baseados em sequências infinitas devem ser usados com muito cuidado, pois não garantem que a rotina de inferência será concluída. O atributo limit é de natureza auxiliar e é usado para organizar cálculos. Não podemos inferir a partir dos valores de outros atributos, deve ser conhecido antes do início do cálculo, por isso foi marcado como obrigatório
Consideramos uma das opções para a aparência de um conceito como função. Mas as questões de segurança e usabilidade de tais conceitos requerem pesquisas mais detalhadas no futuro.
Achatamento de coleções aninhadas
Alguns dialetos SQL que podem trabalhar com dados em forma de objeto suportam uma operação como UNNEST , que converte o conteúdo de uma coleção em um formato de tabela (conjunto de linhas) e adiciona a tabela resultante à cláusula FROM . Geralmente é usado para achatar objetos com estruturas aninhadas, em outras palavras, para achatá-los ou achatá-los. Exemplos dessas linguagens são BigQuery e SQL ++.
Digamos que armazenemos as informações do usuário como um objeto JSON:
[ { "id":1, "alias":"Margarita", "name":"MargaritaStoddard", "nickname":"Mags", "userSince":"2012-08-20T10:10:00", "friendIds":[2,3,6,10], "employment":[{ "organizationName":"Codetechno", "start-date":"2006-08-06" }, { "organizationName":"geomedia", "start-date":"2010-06-17", "end-date":"2010-01-26" }], "gender":"F" }, { "id":2, "alias":"Isbel", "name":"IsbelDull", "nickname":"Izzy", "userSince":"2011-01-22T10:10:00", "friendIds":[1,4], "employment":[{ "organizationName":"Hexviafind", "startDate":"2010-04-27" }] }, …]
Os objetos do usuário armazenam coleções aninhadas com listas de amigos e locais de trabalho. É possível extrair as informações anexadas sobre os locais de trabalho do usuário, colando-as com os dados sobre o usuário retirados do nível superior do objeto usando uma consulta SQL ++:
SELECT u.id AS userId, u.name AS userName, e.organizationName AS orgName FROM Users u UNNEST u.employment e WHERE u.id = 1;
O resultado será:
[ { "userId": 1, "userName": "MargaritaStoddard", "orgName": "Codetechno" }, { "userId": 1, "userName": "MargaritaStoddard", "orgName": "geomedia" } ]
Esta operação é discutida em mais detalhes aqui .
Ao contrário do SQL, no componente de modelagem, os dados embutidos devem ser convertidos não para tabular, mas para formato de objeto. Os conceitos discutidos acima nos ajudarão com isso - um conceito definido por meio de uma função e um conceito anônimo. Um conceito por meio de uma função permitirá que você transforme uma coleção aninhada em formato de objeto, e um conceito anônimo permitirá que você insira sua definição na lista de conceitos pai e acesse os valores de seus atributos contendo a coleção aninhada desejada.
Uma vez que a definição completa de um conceito por meio de uma função é muito complicada para ser usada como um conceito anônimo:
concept conceptName(attribute1, attribute2, ...) by function(query, mode) {...}
precisamos encontrar uma maneira de encurtá-lo. Primeiro, você pode se livrar do título da definição da função com os parâmetros de consulta e modo . Na posição do conceito pai, o argumento de modo será sempre "encontre todos os valores de conceito". O argumento da consulta sempre estará vazio, uma vez que as dependências dos atributos de outros conceitos podem ser embutidas no corpo da função. A palavra-chave do conceito também pode ser eliminada. Assim, obtemos:
conceptName(attribute1, attribute2, ...) {…}
Se o nome do conceito não for importante, ele pode ser omitido e gerado automaticamente:
(attribute1, attribute2, ...) {…}
Se no futuro for possível criar um compilador que possa deduzir uma lista de atributos a partir do tipo de objetos retornados por uma função, então a lista de atributos pode ser descartada:
{…}
Portanto, um exemplo com usuários e seus locais de trabalho como um conceito ficaria assim:
concept userEmployments ( userId = u.id, userName = u.name, orgName = e.orgName ) from users u, {u.employment.map((item) => {orgName: item.organizationName}).iterator()} e
A solução acabou sendo um pouco prolixa, mas universal. Ao mesmo tempo, se nenhuma transformação dos objetos da coleção aninhada for necessária, ela pode ser significativamente simplificada:
concept userEmployments ( userId = u.id, userName = u.name, orgName = e. organizationName ) from users u, {u.employment.iterator()} e
descobertas
Este artigo enfocou duas questões. Primeiro, a transferência de alguns recursos da linguagem SQL para o componente de modelagem: consultas aninhadas, junções externas, agregações e junções com coleções aninhadas. Em segundo lugar, a introdução de duas novas construções no componente de modelagem: definições anônimas de conceitos e conceitos definidos por meio de uma função.
Um conceito anônimo é uma forma abreviada de uma definição de conceito, destinada a ser usada como argumentos para funções ( localizar , localizar um e existir ) ou como uma definição de conceito aninhada em uma cláusula where... Pode ser considerado análogo às definições de funções anônimas em linguagens de programação funcionais.
Um conceito definido por meio de uma função é um conceito, cuja forma de gerar objetos é expressa por meio de um algoritmo de forma explícita. É uma espécie de "interface" entre os mundos da programação funcional ou orientada a objetos e a programação lógica. Será útil em muitos casos quando uma forma lógica de definir um conceito não é conveniente ou impossível: por exemplo, para carregar os fatos iniciais de seus bancos de dados, arquivos ou de solicitações a serviços remotos, para substituir a busca lógica universal por sua implementação otimizada ou para implementar quaisquer regras arbitrárias de criação de objetos.
Curiosamente, empréstimos do SQL, como consultas aninhadas, junções externas e junções de coleção aninhadas, não exigiam grandes mudanças na lógica do componente de modelagem e foram implementados usando conceitos como conceitos anônimos e conceitos por meio de uma função. Isso sugere que esses tipos de conceitos são ferramentas flexíveis e versáteis com grande poder expressivo. Acho que existem muitas maneiras mais interessantes de usá-los.
Portanto, neste e em dois artigos anteriores, descrevi os conceitos básicos e os elementos do componente de modelagem de uma linguagem de programação híbrida. Mas, antes de passar para as questões de integração de um componente de modelagem com um componente de computação que implementa um estilo de programação funcional ou orientado a objetos, decidi dedicar o próximo artigo às suas possíveis opções para sua aplicação. Do meu ponto de vista, o componente de modelagem tem vantagens sobre as linguagens de consulta tradicionais, principalmente SQL, e pode ser usado sozinho, sem integração profunda com o componente de computação, o que desejo demonstrar com vários exemplos no próximo artigo.
O texto científico completo em inglês está disponível em: papers.ssrn.com/sol3/papers.cfm?abstract_id=3555711
Links para publicações anteriores:
Projetando uma linguagem de programação multiparadigma. Parte 1 - Para que serve?
Nós projetamos uma linguagem de programação multiparadigma. Parte 2 - Comparação de modelos de construção em PL / SQL, LINQ e GraphQL Nós
projetamos uma linguagem de programação multiparadigma. Parte 3 - Visão geral das linguagens de representação do conhecimento Nós
projetamos uma linguagem de programação multiparadigma. Parte 4 - Construções básicas da linguagem de modelagem Nós
projetamos uma linguagem de programação multiparadigma. Parte 5 - Características da Programação Lógica