Nous concevons un langage de programmation multi-paradigme. Partie 1 - À quoi ça sert?

Habr est un endroit merveilleux où vous pouvez vous sentir libre de partager vos idées (même si elles ont l'air folles). Habr a vu beaucoup de langages de programmation faits maison, et je vais vous parler de mes expériences dans ce domaine. Mais mon histoire sera différente des autres. Premièrement, ce ne sera pas seulement un langage de programmation, mais un langage hybride qui combine plusieurs paradigmes de programmation. Deuxièmement, l'un des paradigmes sera plutôt inhabituel - il sera destiné à une description déclarative du modèle de domaine. Et troisièmement, la combinaison d'outils de modélisation déclarative et d'approches traditionnelles orientées objet ou fonctionnelles dans un même langage peut donner naissance à un nouveau style de programmation original - la programmation orientée ontologie. Je prévois de divulguer principalement des problèmes et des questions théoriques,que j'ai rencontré, et je parle non seulement du résultat, mais aussi du processus de création de la conception d'un tel langage. Il y aura de nombreux examens de la technologie et des approches scientifiques, ainsi que des discours philosophiques. Il y a beaucoup de matériel, il faudra le décomposer en toute une série d'articles. Si vous êtes intéressé par une tâche aussi vaste et complexe, préparez-vous pour une longue lecture et une immersion dans le monde de la logique informatique et des langages de programmation hybrides.



Je décrirai brièvement la tâche principale



Il consiste à créer un tel langage de programmation qui conviendrait à la fois pour décrire le modèle de domaine et pour travailler avec lui. Rendre la description du modèle la plus naturelle possible, compréhensible pour l'homme et proche des spécifications du logiciel. Mais en même temps, il doit faire partie du code dans un langage de programmation à part entière. Pour cela, le modèle aura la forme d'une ontologie et sera constitué de faits concrets, de concepts abstraits et de relations entre eux. Les faits décriront la connaissance directe du domaine, ainsi que les concepts et les relations logiques entre eux - sa structure.



En plus des outils de modélisation, le langage aura également besoin d'outils pour préparer les données initiales du modèle, créer dynamiquement ses éléments, traiter les résultats des requêtes, créer les éléments de modèle plus pratiques à décrire sous forme algorithmique. Tout cela est beaucoup plus pratique à faire en décrivant explicitement la séquence des calculs. Par exemple, en utilisant la POO ou une approche fonctionnelle.



Et, bien sûr, les deux parties de la langue doivent interagir étroitement et se compléter. Pour qu'ils puissent être facilement combinés dans une seule application et résoudre chaque type de problème avec l'outil le plus pratique.



Je commencerai mon histoire par la question de savoir pourquoi même créer un tel langage, pourquoi un langage hybride et où il serait utile. Dans les prochains articles, je prévois de donner un bref aperçu des technologies et des frameworks qui vous permettent de combiner un style déclaratif avec un style impératif ou fonctionnel. De plus, il sera possible de revoir les langages de description des ontologies, de formuler les exigences et les principes de base d'un nouveau langage hybride et, tout d'abord, sa composante déclarative. Enfin, décrivez ses concepts et éléments de base. Après cela, nous examinerons les problèmes qui surviennent lors de l'utilisation conjointe des paradigmes déclaratif et impératif et comment ils peuvent être résolus. Nous analyserons également certains problèmes d'implémentation du langage, par exemple l'algorithme d'inférence. Enfin, regardons l'un des exemples de son application.



Choisir le bon style de langage de programmation est une condition importante pour la qualité du code



Beaucoup d'entre nous ont dû faire face à des projets complexes créés par d'autres personnes. C'est bien si l'équipe a des personnes qui connaissent le code du projet et peuvent expliquer comment cela fonctionne, il y a de la documentation, le code est propre et compréhensible. Mais en réalité, cela se produit souvent d'une manière différente - les auteurs du code ont quitté longtemps avant d'arriver à ce projet, il n'y a pas de documentation du tout, ou il est très fragmentaire et obsolète il y a longtemps, et sur la logique métier du composant requis, un analyste métier ou un projet - le gestionnaire ne peut le dire qu'en termes généraux. Dans ce cas, la propreté et la compréhensibilité du code sont essentielles.

La qualité du code a de nombreux aspects, l'un d'eux est le choix correct du langage de programmation, qui doit correspondre au problème en cours de résolution. Plus un développeur peut facilement et naturellement implémenter ses idées dans le code, plus vite il peut résoudre le problème et moins il fera d'erreurs. Nous avons maintenant un assez grand nombre de paradigmes de programmation parmi lesquels choisir, chacun avec son propre domaine d'application. Par exemple, la programmation fonctionnelle est préférable pour les applications axées sur le calcul car elle offre plus de flexibilité pour structurer, combiner et réutiliser des fonctions qui exécutent des opérations sur les données. Programmation orientée objetsimplifie la création de structures à partir de données et de fonctions grâce à l'encapsulation, l'héritage, le polymorphisme. La POO convient aux applications orientées données. La programmation logique est pratique pour les problèmes basés sur des règles qui nécessitent de travailler avec des types de données complexes et définis de manière récursive tels que des arbres et des graphiques, et convient pour résoudre des problèmes combinatoires. De plus, la programmation réactive, basée sur les événements et multi-agents a ses portées.



Les langages de programmation modernes à usage général peuvent prendre en charge plusieurs paradigmes. La combinaison des paradigmes fonctionnels et POO est devenue courante depuis longtemps.



La programmation de logique fonctionnelle hybride a également une longue histoire, mais elle n'a jamais dépassé le monde académique. Le moins d'attention est accordée à la combinaison de la programmation logique et impérative de la POO (je prévois d'en parler plus en détail dans l'une des prochaines publications). Bien qu'à mon avis, une approche logique puisse être très utile dans le domaine traditionnel de la POO - applications serveur des systèmes d'information d'entreprise. Il vous suffit de le regarder sous un angle légèrement différent.



Pourquoi je trouve le style de programmation déclaratif sous-estimé



Je vais essayer de justifier mon point de vue.



Pour ce faire, considérez ce que peut être une solution logicielle. Ses principaux composants sont: le côté client (bureau, mobile, applications Web); côté serveur (un ensemble de services séparés, de microservices ou une application monolithique); systèmes de gestion de données (bases de données de graphes relationnelles, orientées document, orientées objet, services de mise en cache, index de recherche). Une solution logicielle doit interagir avec plus que des personnes - les utilisateurs. L'intégration avec des services externes qui fournissent des informations via l'API est une tâche courante. En outre, les sources de données peuvent être des documents audio et vidéo, des textes en langage naturel, le contenu de pages Web, des journaux d'événements, des données médicales, des lectures de capteurs, etc.



D'une part, une application serveur stocke des données dans une ou plusieurs bases de données. D'autre part, il répond aux demandes provenant des points de terminaison d'API, traite les messages entrants et répond aux événements. Les structures des messages et des requêtes ne correspondent presque jamais aux structures stockées dans les bases de données. Les formats de données d'entrée / sortie sont conçus pour un usage externe, optimisés pour le consommateur de ces informations et masquent la complexité de l'application. Les formats de données stockées sont optimisés pour leur système de stockage, par exemple pour un modèle de données relationnel. Par conséquent, nous avons besoin d'une couche intermédiaire de concepts qui permettra de combiner entrée / sortie d'application avec des systèmes de stockage de données. En règle générale, cette couche middleware est appelée couche de logique métier et met en œuvre les règles et principes de comportement des objets de domaine.



La tâche consistant à lier le contenu de la base de données aux objets d'application n'est pas non plus facile. Si la structure des tables dans le stockage correspond à la structure des concepts au niveau de l'application, la technologie ORM peut être utilisée. Mais pour les cas plus complexes que l'accès aux enregistrements par clé primaire et opérations CRUD, vous devez allouer une couche de logique distincte pour travailler avec la base de données. En règle générale, le schéma de la base de données est aussi général que possible, de sorte que différents services puissent l'utiliser. Chacun d'eux mappe ce schéma de données à son propre modèle d'objet. La structure de l'application devient encore plus confuse si l'application ne fonctionne pas avec un magasin de données, mais avec plusieurs, de types différents, chargeant des données à partir de sources tierces, par exemple via l'API d'autres services.Dans ce cas, il est nécessaire de créer un modèle de domaine unifié et de mapper les données de différentes sources vers celui-ci.

Dans certains cas, le modèle de domaine peut avoir une structure complexe à plusieurs niveaux. Par exemple, lors de la compilation de rapports analytiques, certains indicateurs peuvent être construits sur la base d'autres, qui à leur tour seront une source pour la construction du troisième, etc. En outre, les données d'entrée peuvent avoir une forme semi-structurée. Ces données n'ont pas de schéma strict, comme, par exemple, dans le modèle de données relationnel, mais elles contiennent toujours une sorte de balisage qui vous permet d'en extraire des informations utiles. Des exemples de telles données peuvent être des ressources du Web sémantique, des résultats d'analyse de pages Web, des documents, des journaux d'événements, des lectures de capteurs, des résultats de prétraitement de données non structurées telles que des textes, des vidéos et des images, etc. Le schéma de données de ces sources sera construit exclusivement au niveau de l'application. Il y aura aussi un code,convertir les données source en objets de logique métier.



Ainsi, l'application contient non seulement des algorithmes et des calculs, mais également une grande quantité d'informations sur la structure du modèle de domaine - la structure de ses concepts, leurs relations, leur hiérarchie, des règles pour construire certains concepts sur la base d'autres, des règles pour transformer des concepts entre différentes couches de l'application, etc. Lorsque nous rédigeons une documentation ou un projet, nous décrivons ces informations de manière déclarative - sous forme de structures, de diagrammes, d'énoncés, de définitions, de règles, de descriptions en langage naturel. Il est commode pour nous de penser de cette façon. Malheureusement, il n'est pas toujours possible d'exprimer ces descriptions de la même manière naturelle dans le code.



Prenons un petit exemple et spéculons à quoi ressemblera son implémentation en utilisant différents paradigmes de programmation



Disons que nous avons 2 fichiers CSV. Dans le premier fichier:



La première colonne contient l'ID client.

Le second contient la date.

Dans le troisième - le montant facturé,

Dans le quatrième - le montant du paiement.


Dans le deuxième fichier:

La première colonne stocke l'ID client.

Dans le second, le nom.

Le troisième est l'adresse e-mail.


Introduisons quelques définitions:

La facture comprend l'identifiant du client, la date, le montant facturé, le montant du paiement et la dette à partir des cellules d'une ligne du fichier 1.

Le montant de la dette est la différence entre le montant facturé et le montant du paiement.

Le client est décrit à l'aide de l'ID client, du nom et de l'adresse e-mail des cellules d'une ligne du fichier 2.

Une facture impayée est une facture de dette positive.

Les comptes sont liés à un client par la valeur de l'ID client.

Un débiteur est un client qui a au moins une facture impayée dont la date est antérieure d'un mois à la date actuelle.

Un défaillant malveillant est un client qui a plus de 3 factures impayées.


De plus, à l'aide de ces définitions, il est possible de mettre en œuvre la logique d'envoi d'un rappel à tous les débiteurs, de transmettre des données sur les défaillants malveillants aux collecteurs, de calculer une pénalité sur le montant de la dette, de compiler divers rapports, etc.



Dans les langages de programmation fonctionnelsune telle logique métier est mise en œuvre à l'aide d'un ensemble de structures de données et de fonctions pour leur transformation. De plus, les structures de données sont fondamentalement séparées des fonctions. En conséquence, le modèle, et en particulier ses composants tels que les relations entre entités, est caché à l'intérieur d'un ensemble de fonctions, étalé sur le code du programme. Cela crée un grand écart entre la description déclarative du modèle et son implémentation logicielle et complique sa compréhension. Surtout si le modèle a un gros volume.



Structurer un programme en orienté objetle style aide à atténuer ce problème. Chaque entité de domaine est représentée par un objet dont les champs de données correspondent aux attributs de l'entité. Et les relations entre entités sont implémentées sous la forme de relations entre objets, en partie basées sur les principes de la POO - héritage, abstraction de données et polymorphisme, en partie - en utilisant des modèles de conception. Mais dans la plupart des cas, les relations doivent être implémentées en les encodant dans des méthodes objet. De plus, en plus de créer des classes qui représentent des entités, vous aurez également besoin de structures de données pour les ordonner, d'algorithmes pour remplir ces structures et y rechercher des informations.



Dans l'exemple avec les débiteurs, nous pouvons décrire des classes qui décrivent la structure des concepts "Compte" et "Client". Mais la logique de création d'objets, de liaison des objets compte et client entre eux est souvent implémentée séparément dans les classes ou méthodes d'usine. Pour les concepts de débiteurs et de factures impayées, des classes distinctes ne sont pas du tout nécessaires, leurs objets peuvent être obtenus en filtrant les clients et les factures là où ils sont nécessaires. En conséquence, certains des concepts du modèle seront implémentés en tant que classes explicitement, certains implicitement, au niveau de l'objet. Certaines des relations entre les concepts se trouvent dans les méthodes des classes correspondantes, et d'autres sont séparées. La mise en œuvre du modèle sera étalée à travers les classes et les méthodes, mélangée à la logique auxiliaire de son stockage, recherche, traitement, conversion de format. Il faudra quelques efforts pour trouver ce modèle dans votre code et le comprendre.



Le plus proche de la description sera la mise en œuvre du modèle conceptuel dans les langages de représentation des connaissances . Des exemples de tels langages sont Prolog, Datalog, OWL, Flora et autres. J'ai l'intention de parler de ces langues dans la troisième publication. Ils sont basés sur la logique du premier ordre ou ses fragments, par exemple la logique descriptive. Ces langages permettent sous une forme déclarative de spécifier la spécification de la solution au problème, de décrire la structure de l'objet ou du phénomène modélisé et le résultat attendu. Et les moteurs de recherche intégrés trouveront automatiquement une solution qui répond aux conditions spécifiées. La mise en œuvre du modèle de domaine dans ces langages sera extrêmement concise, compréhensible et proche de la description en langage naturel.



Par exemple, la mise en œuvre du problème avec les débiteurs dans Prolog sera très proche des définitions de l'exemple. Pour ce faire, les cellules du tableau devront être représentées comme des faits et les définitions de l'exemple - comme des règles. Pour comparer les comptes et les clients, il suffit de spécifier la relation entre eux dans la règle, et leurs valeurs spécifiques seront affichées automatiquement.



Tout d'abord, nous déclarons les faits avec le contenu des tables au format: ID de table, ligne, colonne, valeur:



cell(“Table1”,1,1,”John”). 


Ensuite, nous donnons des noms à chacune des colonnes:



clientId(Row, Value) :- cell(“Table1”, Row, 1, Value).


Ensuite, vous pouvez combiner toutes les colonnes en un seul concept:



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.



Les difficultés commenceront lors du travail avec le modèle: lors de la mise en œuvre de la logique d'envoi de messages, de transfert de données vers d'autres services, de calculs algorithmiques complexes. Le point faible de Prolog est sa description des séquences d'actions. Leur mise en œuvre déclarative, même dans des cas simples, peut paraître très peu naturelle et nécessite des efforts et des compétences considérables. De plus, la syntaxe de Prolog n'est pas très proche du modèle orienté objet, et les descriptions de concepts composés complexes avec beaucoup d'attributs seront assez difficiles à comprendre.



Comment concilier le langage de développement fonctionnel ou orienté objet traditionnel avec la nature déclarative du modèle de domaine?



L'approche la plus connue est la conception orientée objet (Domain-Driven Design). Cette méthodologie facilite la création et la mise en œuvre de modèles de domaine complexes. Il impose que tous les concepts de modèle soient exprimés explicitement dans le code dans la couche de logique métier. Les concepts du modèle et les éléments du programme qui les mettent en œuvre doivent être aussi proches que possible les uns des autres et correspondre à un langage unique, compréhensible tant pour les programmeurs que pour les experts en la matière.



Un modèle de domaine riche pour l'exemple avec débiteurs contiendra en outre des classes pour les concepts "Facture impayée" et "Débiteur", des classes agrégées pour combiner les concepts de comptes et de clients, des usines pour créer des objets. La mise en œuvre et le support d'un tel modèle prennent plus de temps et le code est lourd - ce qui pouvait auparavant être fait en une seule ligne nécessite plusieurs classes dans un modèle riche. Par conséquent, en pratique, cette approche n'a de sens que lorsque de grandes équipes travaillent sur des modèles réduits complexes.



Dans certains cas, la solution peut être une combinaison d'un langage de programmation fonctionnel ou orienté objet de base et d'un système de représentation des connaissances externe.... Le modèle de domaine peut être transféré vers une base de connaissances externe, par exemple dans Prolog ou OWL, et le résultat des requêtes qui y sont soumises est traité au niveau de l'application. Mais cette approche complique la solution, les mêmes entités doivent être implémentées dans les deux langues, l'interaction entre elles doit être mise en place via l'API, en plus supportée par le système de représentation des connaissances, etc. Par conséquent, il n'est justifié que si le modèle est grand et complexe, nécessitant une inférence logique. Pour la plupart des tâches, ce sera exagéré. De plus, ce modèle ne peut pas toujours être détaché sans douleur de l'application.



Une autre option pour combiner des bases de connaissances et des applications POO est la programmation orientée ontologie.... Cette approche est basée sur les similitudes entre les outils de description d'ontologie et le modèle de programmation objet. Les classes, entités et attributs d'ontologie écrits, par exemple, dans le langage OWL, peuvent être automatiquement mappés aux classes, aux objets et à leurs champs du modèle objet. Et puis les classes résultantes peuvent être utilisées avec d'autres classes de l'application. Malheureusement, la mise en œuvre de base de cette idée aura une portée plutôt limitée. Les langages d'ontologie sont assez expressifs et tous les composants d'ontologie ne peuvent pas être convertis en classes POO d'une manière simple et naturelle. De plus, pour implémenter une inférence à part entière, il ne suffit pas de créer un ensemble de classes et d'objets. Il a besoin d'informations sur les éléments de l'ontologie sous une forme explicite, par exemple sous forme de méta-classes.J'ai l'intention de parler de cette approche plus en détail dans l'une des publications suivantes.



Il existe également une approche aussi extrême du développement logiciel que le développement piloté par un modèle . Selon lui, la tâche principale du développement devient la création de modèles de domaine, à partir desquels le code du programme est ensuite généré automatiquement. Mais dans la pratique, une solution aussi radicale n'est pas toujours suffisamment flexible, notamment en termes de performance des programmes. Le créateur de tels modèles doit combiner les rôles de programmeur et d'analyste commercial. Par conséquent, cette approche ne pouvait pas évincer les approches traditionnelles de mise en œuvre du modèle dans les langages de programmation à usage général.



Toutes ces approches sont assez lourdes et ont du sens pour des modèles d'une grande complexité, souvent décrits séparément de la logique de leur utilisation. J'aimerais quelque chose de plus léger, plus confortable et plus naturel. De sorte qu'avec l'aide d'un langage, il était possible de décrire à la fois le modèle sous une forme déclarative et les algorithmes pour son utilisation. Par conséquent, j'ai réfléchi à la manière de combiner le paradigme orienté objet ou fonctionnel (appelons-le le composant de calcul ) et le paradigme déclaratif (appelons-le le composant de modélisation ) dans un seul langage de programmation hybride . À première vue, ces paradigmes semblent opposés les uns aux autres, mais plus il est intéressant d'essayer.



Ainsi, l'objectif est de créer un langage qui devrait être à l'aise pour la modélisation conceptuelle basée sur des données semi-structurées et disparates. La forme du modèle doit être proche de l'ontologie et consister en une description des entités du domaine et des relations entre elles. Les deux composants du langage doivent être étroitement intégrés, y compris au niveau sémantique.



Les éléments de l'ontologie doivent être des entités du langage de premier niveau - ils peuvent être passés aux fonctions en tant qu'arguments, affectés à des variables, etc. Puisque le modèle-ontologie deviendra l'un des éléments principaux du programme, alors cette approche de la programmation peut être appelée orientée ontologiquement. Combiner la description du modèle avec les algorithmes pour son utilisation rendrait le code du programme plus compréhensible et naturel pour l'homme, le rapprocherait du modèle conceptuel du domaine et simplifierait le développement et la maintenance du logiciel.



Assez pour la première fois. Dans le prochain article, je veux parler de certaines technologies modernes qui combinent des styles impératif et déclaratif - PL / SQL, Microsoft LINQ et GraphQL. Pour ceux qui ne veulent pas attendre la sortie de toutes les publications sur Habré, il existe un texte intégral de style scientifique en anglais, disponible sur le lien:

Programmation orientée ontologie hybride pour le traitement de données semi-structurées .



All Articles