我们设计了一种多范例编程语言。第1部分-目的是什么?

哈伯(Habr)是一个绝佳的地方,您可以随时分享您的想法(即使它们看起来很疯狂)。 Habr看到了许多自制的编程语言,我将向您介绍我在这一领域的实验。但是我的故事将与其他故事不同。首先,它将不仅仅是一种编程语言,而是一种结合了几种编程范例的混合语言。其次,其中一个范例将非常不寻常-它将用于域模型的声明性描述。第三,将声明式建模工具与一种语言中的传统面向对象或功能方法结合使用,可以产生一种新的原始编程风格-面向本体的编程。我打算主要公开理论上的问题和疑问,我遇到的问题,并且不仅要讲述结果,而且要讲述创建这种语言的设计过程。技术和科学方法以及哲学话语将有许多评论。有很多资料,您必须将其分解为一系列文章。如果您对如此大规模和复杂的任务感兴趣,请准备好长时间阅读和沉浸在计算机逻辑和混合编程语言的世界中。



我将简要描述主要任务



它在于创建一种这样的编程语言,该语言既方便描述领域模型,又可以方便地使用它。为了使模型的描述尽可能自然,易于理解并且接近软件的规格。但是同时,它必须是成熟的编程语言中代码的一部分。为此,该模型将具有本体的形式,并将由具体事实,抽象概念及其之间的关系组成。事实将描述主题领域的直接知识,以及它们之间的概念和逻辑关系-结构。



除了建模工具之外,该语言还将需要工具来为模型准备初始数据,动态创建其元素,处理对它的查询结果,创建更便于以算法形式描述的那些模型元素。通过明确描述计算顺序,所有这些操作都更加方便。例如,使用OOP或功能方法。



而且,当然,语言的两个部分都必须紧密交互并相互补充。以便可以将它们轻松组合到一个应用程序中,并使用最方便的工具解决每种类型的问题。



我的故事将从一个问题开始,即为什么还要创建这样的语言,为什么要使用混合语言以及在哪里有用。在下一篇文章中,我计划简要概述技术和框架,使您可以将声明式样式与命令式或功能性结合起来。此外,将有可能审查用于描述本体的语言,制定新的混合语言的要求和基本原理,首先是其声明性组件。最后,描述其基本概念和元素。之后,我们将考虑同时使用声明式和命令式范式时会出现什么问题以及如何解决它们。我们还将分析语言实现的一些问题,例如推理算法。最后,让我们看一下其应用程序的示例之一。



选择正确的编程语言样式是代码质量的重要条件



我们许多人不得不处理对其他人创建的复杂项目的支持。如果团队中有熟悉项目代码并可以解释项目代码的人员,那是很好的。有文档,代码清晰易懂。但是实际上,它通常以不同的方式发生-代码作者在进入该项目之前很久就退出了,根本没有文档,或者很零碎而且很久以前已经过时,并且涉及所需组件,业务分析师或项目的业务逻辑。 -经理只能笼统地说。在这种情况下,代码的整洁性和可理解性至关重要。

代码的质量有很多方面,其中之一就是正确选择编程语言,这应该与要解决的问题相对应。开发人员越容易在代码中实现其想法,就越自然,解决问题的速度就越快,所犯的错误也就越少。我们现在有相当多的编程范例可供选择,每种都有自己的应用领域。例如,函数式编程对于以计算为重点的应用程序更为可取,因为它为结构化,组合和重用对数据执行操作的函数提供了更大的灵活性。面向对象编程通过封装,继承,多态性,简化了根据数据和功能创建结构的过程。 OOP适用于面向数据的应用程序。对于需要使用复杂的,递归定义的数据类型(例如树和图)的基于规则的问题,逻辑编程非常方便,并且适合于解决组合问题。同样,反应式,事件驱动的多主体编程也有其范围。



现代通用编程语言可以支持多种范例。长期以来,功能和OOP范式的结合一直是主流。



混合函数逻辑编程也有悠久的历史,但从未超越学术领域。最少要注意逻辑和命令式OOP编程的结合(我计划在下一本书中更详细地讨论它们)。尽管在我看来,逻辑方法在OOP的传统领域(公司信息系统的服务器应用程序)中可能非常有用。您只需要从稍微不同的角度看待它。



为什么我发现声明式编程风格被低估了



我将尝试证实我的观点。



为此,请考虑可以使用什么软件解决方案。它的主要组件是:客户端(桌面,移动,Web应用程序);服务器端(一组单独的服务,微服务或单片应用程序);数据管理系统(关系型,面向文档,面向对象,图形数据库,缓存服务,搜索索引)。软件解决方案不仅需要与人(用户)进行交互。与通过API提供信息的外部服务集成是一项常见的任务。同样,数据源可以是音频和视频文档,自然语言文本,网页内容,事件日志,医疗数据,传感器读数等。



一方面,服务器应用程序将数据存储在一个或多个数据库中。另一方面,它响应来自API终结点的请求,处理传入消息并响应事件。消息和查询的结构几乎从不匹配存储在数据库中的结构。输入/输出数据格式是为外部使用而设计的,针对此信息的使用者进行了优化,并隐藏了应用程序的复杂性。已针对其存储系统(例如,关系数据模型)优化了存储的数据格式。因此,我们需要一些概念的中间层,以允许将应用程序输入/输出与数据存储系统结合在一起。通常,该中间件层称为业务逻辑层,并为域中的对象实现行为的规则和原则。



将数据库内容链接到应用程序对象的任务也不容易。如果存储中表的结构与应用程序级别的概念结构匹配,则可以使用ORM技术。但是,对于比通过主键和CRUD操作访问记录更为复杂的情况,您必须分配一个单独的逻辑层来处理数据库。通常,数据库架构尽可能通用,以便不同的服务可以使用它。每个模型都将此数据模式映射到其自己的对象模型。如果应用程序不使用一个数据存储,而是使用几种不同类型的数据,例如通过其他服务的API从第三方源加载数据,则应用程序的结构将变得更加混乱。在这种情况下,有必要创建一个统一的域模型并将不同来源的数据映射到该模型。

在某些情况下,领域模型可以具有复杂的多层结构。例如,在编制分析报告时,可以在其他指标的基础上构建某些指标,而这些指标又将是构建第三个指标的来源,等等。此外,输入数据可以具有半结构化形式。该数据没有严格的架构,例如在关系数据模型中,但是它仍然包含某种标记,可让您从中提取有用的信息。此类数据的示例可以是语义Web资源,Web抓取结果,文档,事件日志,传感器读数,非结构化数据(例如文本,视频和图像)的预处理结果等。这些源的数据模式将完全在应用程序级别构建。还会有一个代码将源数据转换为业务逻辑对象。



因此,该应用程序不仅包含算法和计算,还包含有关领域模型结构的大量信息-其概念的结构,它们的关系,层次结构,在其他概念的基础上构建某些概念的规则,在应用程序不同层之间转换概念的规则等。当我们起草文档或项目时,我们以自然语言的结构,图表,陈述,定义,规则,描述的形式声明性地描述此信息。我们以这种方式思考很方便。不幸的是,并非总是能够以相同的自然方式在代码中表达这些描述。



让我们考虑一个小示例,并使用不同的编程范例来推测其实现的样子



假设我们有2个CSV文件。在第一个文件中:



第一列包含客户端ID。

第二个包含日期。

在第三部分-发票金额,

在第四部分-付款金额。


在第二个文件中:

第一列存储客户端ID。

第二,名字。

第三个是电子邮件地址。


让我们介绍一些定义:

发票包括客户的标识符,日期,发票金额,付款金额以及来自文件1行的单元格中

的债务债务金额是发票金额与付款金额之间的差额。

使用文件2中一行的单元格中的客户ID,名称和电子邮件地址来描述客户。

未付帐单是正债务帐单。

帐户通过客户ID值链接到客户。

债务人是指至少拥有一张未付款发票的客户,该发票的日期比当前日期早1个月。

恶意违约者是拥有3张以上未付发票的客户。


此外,使用这些定义,您可以实现以下逻辑:向所有债务人发送提醒,将持久性违约者的数据传输给收款人,计算债务金额的罚款,编制各种报告等。



函数式编程语言使用一组数据结构和功能对其进行转换可实现此类业务逻辑。此外,数据结构从根本上与功能分开。结果,模型,尤其是其组件(例如实体之间的关系)被隐藏在一组函数内,这些函数涂抹在程序代码上。这在模型的声明性描述与其软件实现之间造成了很大的差距,并使对它的理解变得复杂。特别是在模型体积较大的情况下。面向对象



的程序结构样式有助于缓解此问题。每个域实体都由一个对象表示,该对象的数据字段对应于该实体的属性。实体之间的关系以对象之间的关系的形式实现,部分基于OOP原理-继承,数据抽象和多态性-部分使用设计模式。但是在大多数情况下,必须通过在对象方法中对其进行编码来实现关系。此外,除了创建代表实体的类之外,您还需要用于对它们进行排序的数据结构,填充这些结构并在其中搜索信息的算法。



在带有债务人的示例中,我们可以描述描述“帐户”和“客户”概念结构的类。但是创建对象,将帐户和客户对象相互链接的逻辑通常是在工厂类或方法中单独实现的。对于债务人和未付发票的概念,根本不需要单独的类,可以通过在需要的地方过滤客户和发票来获得它们的对象。结果,模型的某些概念将以类的形式显式实现,而某些(隐式地)在对象级别上实现。概念之间的某些关系在相应类的方法中,而另一些则是独立的。该模型的实现将在类和方法之间涂抹,并与它的存储,搜索,处理和格式转换的辅助逻辑混合在一起。在您的代码中找到并理解该模型将需要一些努力。



与描述最接近的将是知识表示语言中概念模型的实现。此类语言的示例包括Prolog,Datalog,OWL,Flora等。我计划在第三本出版物中讨论这些语言。它们基于一阶逻辑或其片段,例如描述性逻辑。这些语言允许以声明的形式指定问题解决方案的规范,描述建模对象或现象的结构以及预期结果。内置的搜索引擎将自动找到满足指定条件的解决方案。此类语言中领域模型的实现将非常简洁,易懂且接近自然语言中的描述。



例如,Prolog中债务人问题的实现将非常接近示例中的定义。为此,需要将表单元格表示为事实,并将示例中的定义表示为规则。要比较帐户和客户,在规则中指定它们之间的关系就足够了,它们的特定值将自动显示。



首先,我们用表的内容声明事实,格式为:表ID,行,列,值:



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


然后,我们为每个列命名:



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


然后,您可以将所有列合并为一个概念:



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, _, _, _).


等等。



使用模型时会遇到困难:实现发送消息,将数据传输到其他服务的逻辑,复杂的算法计算时。Prolog的弱点是它对动作序列的描述。即使在简单的情况下,它们的声明式实现也可能看起来非常不自然,并且需要大量的精力和技巧。另外,Prolog的语法与面向对象模型不是很接近,对具有大量属性的复杂复合概念的描述将很难理解。



我们如何使主流的功能性或面向对象的开发语言与领域模型的声明性相一致?



最著名的方法是面向对象的设计(域驱动设计)。这种方法有助于复杂域模型的创建和实施。它要求所有模型概念在业务逻辑层中明确地用代码表示。模型的概念和实现它们的程序元素应尽可能彼此接近,并且对应于一种语言,程序员和主题专家都可以理解。



具有债务人的示例的丰富域模型将另外包含“未付发票”和“债务人”概念的类,用于组合帐户和客户概念的聚合类,用于创建对象的工厂。这种模型的实现和支持更加耗时,并且代码繁琐-以前在一行中可以完成的工作需要在丰富模型中包含多个类。结果,实际上,这种方法仅在大型团队正在研究复杂规模模型时才有意义。



在某些情况下,解决方案可以是基本的功能或面向对象的编程语言与外部知识表示系统的组合。...可以将域模型转移到外部知识库中,例如在Prolog或OWL中,并在应用程序级别处理对其查询的结果。但是这种方法使解决方案变得复杂,必须以两种语言实现相同的实体,必须通过API设置它们之间的交互,并由知识表示系统额外支持,等等。因此,只有在模型庞大且复杂,需要逻辑推理的情况下才是合理的。对于大多数任务,这将是多余的。此外,不能总是轻松地从应用程序中分离该模型。



组合知识库和OOP应用程序的另一个选择是面向本体的编程。...这种方法基于本体描述工具和对象编程模型之间的相似性。例如,以OWL语言编写的类,实体和本体属性可以自动映射到对象模型的类,对象及其字段。然后,可以将所得的类与应用程序的其他类一起使用。不幸的是,这个想法的基本实现将在范围上受到限制。本体语言表达能力很强,并非所有本体组件都可以通过简单自然的方式转换为OOP类。而且,要实现全面的推理,仅创建一组类和对象是不够的。他需要以显式形式(例如,以元类的形式)有关本体元素的信息。我计划在以下出版物之一中更详细地讨论这种方法。



还有一种极端的软件开发方法,例如模型驱动开发。据此,开发的主要任务成为域模型的创建,然后自动从中生成程序代码。但是实际上,这种激进的解决方案并不总是足够灵活,特别是在程序性能方面。这种模型的创建者必须兼顾程序员和业务分析师的角色。因此,这种方法不能挤占以通用编程语言实现模型的传统方法。



所有这些方法都非常麻烦,并且对于非常复杂的模型(通常与它们的使用逻辑分开描述)有意义。我想要更轻,更舒适,更自然的东西。这样一来,借助一种语言,就可以以声明形式描述模型和使用算法。因此,我考虑了如何在单一混合编程语言中结合面向对象或功能性范式(我们称其为计算组件)和声明性范式(我们称其为建模组件。乍一看,这些范式看上去彼此相对,但是尝试起来更有趣。



因此,目标是基于半结构化和完全不同的数据创建一种适合概念建模的语言。模型的形式应接近本体,并由对域实体及其之间关系的描述组成。语言的两个组成部分应紧密集成在一起,包括语义级别。



本体的元素应该是第一层语言的实体-它们可以作为参数传递给函数,分配给变量等。由于模型-本体将成为程序的主要元素之一,因此可以将这种编程方法称为面向本体。将模型的描述与使用算法相结合,将使程序代码对人类而言更易于理解和自然,将其与领域的概念模型更接近,并简化了软件的开发和维护。



第一次就够了。在下一篇文章中,我想讨论一些将命令式和声明式风格相结合的现代技术-PL / SQL,Microsoft LINQ和GraphQL。对于那些不想等待所有有关Habré的出版物的发布的人,可以通过以下链接获得科学英语的全文:链接:

面向混合本体的半结构化数据处理编程



All Articles