声明式编程的梦想[关闭]


26

为什么没有实现声明式编程的梦想?有什么具体的障碍阻碍?举个简单的例子,为什么我不能说

sort(A) is defined by sort(A) in perm(A) && asc(sort(A))

并自动获得排序算法。perm表示排列,asc表示上升。


4
顺便说你的具体例子是那种已经可用:gkoberger.github.io/stacksort
书斋

3
您听说过Prolog吗?只需查找“答案集编程”。许多系统都基于默认逻辑。
schlingel 2015年

16
好吧,这个问题很容易回答。 尝试实施这样的系统。是什么使您无法成功完成?奇妙的是,无论您停止了什么,都阻止了其他所有人。
埃里克·利珀特

4
我很想相信这个问题值得得到更多的赞誉。乍看之下,您可能会想,那很简单!您必须对其背后的所有逻辑进行编程,而计算机并不那么聪明。 ...但是然后您再回头看一下这个问题,然后您再想一想,嗯,是的,这很简单,您必须对所有逻辑进行编程,并且计算机不一定是最出色的工具在棚子里是对的-但这种解释要比表面上的内容要深得多。
Panzercrisis

3
是的,您对排序算法的描述是声明性的,是的,但是可以肯定,因为地狱效率不高。有n!一个序列的置换,在最坏的情况下,您的算法将不得不尝试全部序列以找到排序的序列。阶乘时间与处理序列的算法一样糟糕。
本杰明·霍奇森

Answers:


8

有一些很好的答案。我将尽力为讨论做出贡献。

关于Prolog中的声明性逻辑编程主题,理查德·奥基夫(Richard O'Keefe)撰写了一本很棒的书“ Prolog of Prolog”。它是关于使用一种编程语言来编写高效的程序,该语言可以让您编写效率很低的程序。在本书中,在讨论几种算法的有效实现时(在“编程方法”一章中),作者采用了以下方法:

  • 用英语定义问题
  • 编写一个尽可能声明性的可行解决方案;通常,这几乎意味着您所要解决的问题,只是正确的Prolog
  • 从那里开始,采取措施完善实施以使其更快

通过这些工作,我能够做出的最启发性的发现(对我而言):

是的,实现的最终版本比作者开始使用的“声明性”规范有效得多。它仍然是非常声明,简洁和易于理解的。介于两者之间的是最终解决方案捕获了最初解决方案所忽略的问题的属性。

换句话说,在实施解决方案时,我们会尽可能多地使用我们对问题的了解。比较:

查找列表的排列,以使所有元素都按升序排列

至:

合并两个排序列表将产生一个排序列表。由于可能已经排序了子列表,因此请使用这些列表作为起点,而不要使用长度为1的子列表。

除了一点点:像您给出的定义一样有吸引力,因为它非常笼统。但是,我无法避免这样一种感觉,即它有目的地忽略了排列是组合问题的事实。这是我们已经知道的!这不是批评,而只是观察。

至于真正的问题:如何前进?嗯,一种方法是提供有关我们要向计算机声明的问题的尽可能多的知识。

我真正解决问题的最佳尝试是在亚历山大·斯蒂芬诺夫(Alexander Stepanov)合着的《编程要素》《从数学到泛型编程》中提出的。不幸的是,我没有完成总结(甚至完全理解)这些书中所有内容的任务。然而,在事先知道输入的所有相关属性的前提下,该方法将定义有效(甚至最佳)的库算法和数据结构。最终结果是:

  • 每个明确定义的转换都是对已经存在的约束(已知属性)的改进。
  • 我们让计算机根据现有约束条件来决定哪种转换是最佳的。

至于为什么它还没有发生,那么,计算机科学是一个非常年轻的领域,我们仍在努力真正地欣赏其中大多数的新颖性。

聚苯乙烯

让您了解“完善实现”的含义:例如在Prolog中获取列表最后一个元素的简单问题。规范的声明式解决方案是说:

last(List, Last) :-
    append(_, [Last], List).

在此,声明性含义append/3是:

List1AndList2是的串联List1List2

由于在第二个参数中append/3我们有一个仅包含一个元素的列表,并且第一个参数被忽略了(下划线),因此我们得到了原始列表的拆分,该列表丢弃了列表的开头(List1在的上下文中append/3)并要求后面(List2在的上下文中append/3)确实是只有一个元素的列表:因此,它是最后一个元素。

但是,SWI-Prolog提供实际实现是

last([X|Xs], Last) :-
    last_(Xs, X, Last).

last_([], Last, Last).
last_([X|Xs], _, Last) :-
    last_(Xs, X, Last).

这仍然是很好的声明。从上至下阅读,内容如下:

列表的最后一个元素仅对至少一个元素的列表有意义。那么,一对尾部和列表的头的最后一个元素是:头(当尾部为空时)或最后一个非空尾部。

之所以提供此实现,是为了解决围绕Prolog 执行模型的实际问题。理想情况下,使用哪种实现方式都不应该有所作为。同样,我们可以说:

last(List, Last) :-
    reverse(List, [Last|_]).

列表的最后一个元素是反向列表的第一个元素。

如果您想就声明式Prolog的优点进行无定论的讨论,只需在Stack OverflowProlog标记中进行一些问答。


2
+1用于显示声明性设计如何通过迭代设计过程从简单的抽象发展为更具体的实现。
itsbruce 2015年

1
@鲍里斯这是一个很好的答案。那本书坐在我的书架上。我可能应该打开它了。
davidk01年

1
@ davidk01那里最好的书之一。假定您总体上对Prolog和编程很满意,但是编程所采用的方法既实用又非常彻底。
2015年

2
@Boris我知道示例并不复杂,但是迭代设计过程的生产力-声明性语言的真正优势-以及非常实用的价值至关重要。声明性语言提供了一种清晰,一致,递归的迭代改进方法。祈使语不是。
itsbruce 2015年

1
+1代表“让您对什么是好的,声明性的Prolog进行无定论的讨论”…确实,我们倾向于不同意!
Daniel Lyons

50

逻辑语言已经做到了。您可以像执行操作一样类似地定义排序。

主要问题是性能。计算机可能擅长计算很多东西,但是它们本质上是愚蠢的。程序员可能将计算机可能做出的每个“明智”决定都编程了进去。通常,不是通过最终结果的样子来描述此决定,而是通过逐步实现此最终结果的方式来描述。

想象一下魔像的故事。如果您尝试给他一个抽象的命令,那么充其量是,他将效率低下,最糟糕的是,它将伤害自己,您或其他人。但是,如果您尽可能详细地描述您想要的内容,则可以保证任务将有效有效地完成。

决定使用哪种抽象级别是程序员的工作。对于您正在制作的应用程序,您是否要进行高层描述并以抽象的方式对其进行描述,使性能受到影响或变得低落,肮脏,花费10倍以上的时间,但获得的性能要高1000倍?


6
知道Golemגולם这个词实际上是指“原始材料”,即机器/实体所处的最基本状态,可能会有所帮助。
dotancohen

2
声明性语言本质上不是降低抽象级别的障碍。Haskell和Standard ML都以不同的方式,使您可以在一个地方进行有关类型/函数的简单声明性声明,在单独的位置提供一系列具体而又具体的函数实现,并在另一位置将类型与实现进行匹配。同时,面向对象/命令式语言的最佳实践现在更多地是从高/简单开始,然后添加实现细节。所不同的是,在FP中,较高的抽象更容易,而在层次上势必较容易。
itsbruce 2015年

2
应该说,也有可能以上述两种语言根据类型的属性自动解决对特定实现的选择,而不是对特定匹配进行硬编码,这几乎满足了OP的要求。在Haskell中,类型类将是实现此目的的关键工具。在标准ML中,函子。
itsbruce 2015年

22
@BAR Golem!= Golum Golem来自犹太民俗
欣快感

10
我从这个答案中得到的收获是在笔记本电脑上写了אמת。
Dan J

45

除了Euphoric的优点之外,我想补充一点,我们已经在许多运行良好的地方使用了声明性语言,即描述状态不太可能改变或请求计算机实际上可以为其生成有效代码的内容在其自己的:

  • HTML声明了网页的内容。

  • CSS声明网页中各种元素的外观。

  • 每个关系数据库都有一种数据定义语言,用于声明数据库的结构。

  • SQL更接近于声明式,而不是命令式,因为您告诉了您想看的内容,并且数据库的查询计划器确定了如何使其真正实现。

  • 有人可能会说大多数配置文件(.vimrc,.profile,.bashrc,.gitconfig)使用的是特定于域的语言,该语言在很大程度上是声明性的


3
我将提到GNU Make,XSLT,Angular.js,它们也是具有声明性的广泛使用的东西(尽管angular可能会稍微推动该定义)。
Mark K Cowan'3

让我将正则表达式添加到该列表。
Schwern 2015年

7
人们往往会忘记声明式语言是常见的。他们只是通常不会讲完整的语言。将正则表达式添加到该列表。
slebetman'3

有点古怪,但仍然:并不是每个数据库都有DDL,只是想想大量的无模式NoSQL数据库。每个关系数据库可能都有,但不是每个数据库都有。
恢复莫妮卡-迪尔克15-3-10

1
@dirkk没有想到这一点。更正了我的答案。
Ixrec 2015年

17

抽象是泄漏的

您可以在声明性系统中声明所需内容,然后编译器或解释器会确定执行顺序。从理论上讲,它的好处是使您不必考虑“如何做”,而不必详细说明此实现。但是,在实践中,对于通用计算,您仍然必须考虑“如何”并编写各种技巧,同时牢记如何实现,因为否则编译器可以(并且经常会)选择将要实现的实现。非常非常非常慢(例如n!个操作,其中n就足够了)。

在您的特定示例中,您将获得A排序算法-这并不意味着您将获得一个好甚至是有点可用的算法。您给定的定义,如果按字面意义实现(可能会像编译器一样),则会导致http://en.wikipedia.org/wiki/Bogosort不能用于较大的数据集-从技术上讲是正确的,但需要永恒才能对一千个数字进行排序。

对于某些有限的域,您可以编写在确定良好实现方面几乎总是表现出色的系统,例如SQL。对于效果不佳的通用计算-您可以在Prolog中编写系统,但是您必须可视化最终将如何将声明准确转换为命令式执行顺序,并且会损失很多预期的声明式编程的好处。


虽然您所说的基本上是正确的,但是性能差并不是抽象泄漏的迹象,除非接口/合同为您提供了有关执行时间的保证。
valenterry 2015年

3
彼得斯并不是在说性能差是抽象泄漏的迹象,@ valenterry。如果有的话,他说的恰恰相反:要获得良好的性能,实施细节将被迫泄漏。
itsbruce 2015年

2
我认为将抽象泄漏只是因为您需要了解实现以了解它如何影响性能而产生的误导。抽象的目的并不是要让您不必考虑性能。
2015年

1
@jamesqf在声明式编程中,您只需要声明某些内容已排序即可。您可以声明排序顺序已绑定到某些变量/属性。然后会是这样。每次添加新数据或更改排序顺序时,无需显式调用sort。
海德

1
@jamesqf如果不实际尝试就无法真正理解要点(我建议使用Qt的QML来处理声明性想法)。想象一个只知道命令式编程的人,他们试图了解OOP或函数式编程的要点,而没有实际尝试过。
海德

11

计算可确定性是声明性编程尚未被证明像看起来那样容易的最重要原因。

事实证明,许多相对容易陈述的问题是无法决定的,或者需要解决NP完全复杂的问题。当我们考虑否定类和分类,可数性和递归时,通常会发生这种情况。

我想用一些众所周知的领域来体现这一点。

决定使用哪个CSS类需要知识和对所有CSS规则的考虑。添加新规则可能会使所有其他决定无效。由于NP完全问题,有意不将负CSS类添加到语言中,但是缺少负类会使CSS设计决策复杂化。

在(SQL)查询优化器中,存在一个棘手的问题,即决定以哪种顺序进行连接,使用哪些索引以及将哪些内存分配给哪些临时结果。这是一个已知的NP完全问题,使数据库设计和查询制定变得复杂。用不同的方式表述:在设计数​​据库或查询时,设计人员需要知道查询优化器可能采取的操作和操作顺序。经验丰富的工程师需要了解主要数据库供应商使用的启发式技术。

配置文件是声明性的,但是某些配置很难声明。例如,要正确配置功能,需要考虑版本控制,部署(和部署历史记录),可能的手动替代以及与其他设置的冲突。正确验证配置可能会成为NP完全问题。

结果是这些复杂性使初学者感到惊讶,它们破坏了声明式编程的“美”,并导致一些工程师寻找其他解决方案。没有经验的工程师从SQL迁移到NoSQL可能是关系数据库的潜在复杂性触发的。


2
“由于NP完全问题,有意不将负CSS类添加到该语言中” –您能否详细说明?
约翰·德沃夏克

这是一个练习,但是如果使用否定的CSS选择器,则可以将其重写为3SAT问题(最后一个子句为DOM),这需要尝试所有可能的组合以查看其是否匹配。
Dibbeke 2015年

1
小加成。在CSS 3和4中,允许使用否定选择器,但:not伪类不得嵌套。
Dibbeke 2015年

2

我们在编程语言的声明性方面存在差异,可以很好地用于数字逻辑的验证。

通常,数字逻辑以寄存器传输级(RTL)进行描述,其中定义了寄存器之间信号的逻辑级。为了检查我们是否越来越增加以更抽象和声明性的方式定义的属性。

更具说明性的语言/语言子集之一称为“属性规范语言”的PSL。在测试乘法器的RTL模型时,例如,需要指定移位和加法多个时钟周期的所有逻辑运算;您可以通过编写属性assert that when enable is high, this output will equal the multiplication of these two inputs after no more than 8 clock cycles。然后可以在仿真中将PSL描述与RTL一起进行检查,或者可以正式证明PSL适用于RTL描述。

更具声明性的PSL模型迫使人们描述与RTL描述相同的行为,但是要采用足够不同的方式,可以根据RTL自动检查其是否一致。


1

通常,问题在于如何对数据建模;声明式编程在这里没有帮助。在命令式语言中,您已经拥有大量的库,可以为您做很多事情,因此您只需要知道要调用的内容。在具体的方式可以考虑这个声明性编程(可能是最好的例子是流API的Java 8)。有了这个,抽象就已经解决了,不需要声明式编程。

而且,正如已经说过的那样,逻辑编程语言已经完全可以满足您的要求。也许有人会说问题出在性能上,但是通过当今该领域的硬件和研究,可以改进一切以准备投入生产。Prolog实际上是用于AI的东西,但我相信只有学术界才能使用。

要注意的是,它适用于通用编程语言。对于特定领域的语言,声明性语言会更好。SQL可能是最好的例子。


3
数据建模?您选择了当务之急是最糟糕的事情。像Haskell和ML这样的声明性功能语言擅长于数据建模。例如,代数数据类型和递归数据类型通常可以在一两行中进行全面定义。当然,您仍然可以编写函数,但是它们的代码无可挑剔地遵循类型定义,并受其约束。比较古怪。
itsbruce 2015年

1
@itsbruce最重要的是,大多数真实数据不容易映射到ADT。想一想大多数数据库的工作方式。作为Prolog-Erlang,您是对的,它们是不同的语言。我提到一个是功能性的,另一个是逻辑性的,但是如果我删除整个比较,那是最好的。
m3th0dman 2015年

1
@ m3th0dman数据库只是大量的元组/记录。Haskell的位置有点残缺,因为它没有记录,但确实有元组,而ML都有。而且在Haskell的情况下,声明新的伪记录数据类型所需的样板数量仍然比使用平均静态类型的OOP语言创建仿造结构所需的数量少得多。您能否详细说明大多数数据不容易映射到ADT?
2015年

1
@ m3th0dman啊,这就是为什么用非常适合该任务的命令性语言定义数据库模式的原因。哦,不,那将是声明性的DDL。实际上,数据建模的整个过程与将要使用它的应用程序,数据流和结构有关,而不与实现该应用程序的语言有关。有时,它们会失真以匹配某种语言的OO功能及其ORM支持的功能,但这通常是一件坏事,而不是功能。声明性语言更适合表达概念/逻辑数据模型。
itsbruce 2015年

1
@itbruce我并不是说过程范式在定义数据方面比声明式范式更好。我说的是,声明性范例并不比程序性范例(对于通用语言)更好(也不更糟)。至于处理数据,SQL的声明部分对于现实生活中的应用程序是不够的。否则,没人会发明和使用程序扩展。至于文章,我不同意与布鲁克斯相矛盾的摘要。他从真实的项目中建立了自己的想法,而那些家伙并没有建立任何出色的证据来证明他们的理论。
m3th0dman'3

0

看起来像这样。。{(无论如何=>读取文件并调用url)| 调用url并读取文件}}但是,这些是要执行的操作,因此系统状态会发生变化,但是从源头上来看并不明显。

陈述式可以描述有限状态机及其转换。FSM就像没有动作的声明式的反义词,即使唯一的动作就是将状态更改为下一个状态。

使用此方法的优势在于,可以通过谓词指定过渡和动作,这些谓词不仅适用于多个过渡,而且适用于多个过渡。

我知道这听起来有些奇怪,但是在2008年,我编写了一个使用此方法的程序生成器,生成的C ++是源代码的2到15倍。我现在有20,000行输入中的超过75,000行C ++。这有两点:一致性和完整性。

一致性:没有两个可以同时成立的谓词可以暗示动作不一致,因为x = 8和x = 9,也没有不同的下一个状态。

完整性:指定了每个状态转换的逻辑。对于具有> 2 ** N个状态的N个子状态的系统,这些方法可能很难检查,但是有一些有趣的组合方法可以验证所有内容。1962年,我使用这种条件代码生成和组合调试为7070机器编写了系统分类的第一阶段。在排序的8,000行中,自首次发布之日起,错误的数量永远是零!

此类的第二阶段,即12,000行,在前两个月内发生了60多个错误。关于这一点,还有很多要说的,但是它有效。如果汽车制造商使用此方法检查固件,则不会看到我们现在看到的故障。


1
这确实不能回答原始问题。一致性和完整性如何影响大多数编程仍然是过程性而非声明性的事实?
杰伊·埃尔斯顿

你的首段似乎是一个答案,一个点阿尔诺的回答programmers.stackexchange.com/a/275839/67057,而不是问题本身。在那里应该是一条评论(在我的屏幕上,您的答案不再低于他的回答)。我您的答案的其余部分说明了少量的声明性代码如何生成大量等效的命令性代码,但目前尚不清楚。您的答案需要整理一下,尤其是在要点上。
itsbruce

-3

并非所有内容都能以声明方式表示。

通常,您明确地控制执行流程

例如下面的伪代码: if whatever read a file call an URL else call an URL write a file 您将如何声明性地表示它?

当然,可能有一些特殊的方法可以做到这一点。像单子。但是这些通常比它们的过程部分更麻烦,更复杂并且不那么直观。

归结为以下事实:与您的环境/系统“交互” 不是声明性的。本质上,与I / O相关的所有过程都是过程性的。您必须准确说明何时,什么情况以及应按什么顺序进行。

声明式对于纯粹与计算有关的所有事情都非常有用。就像一个巨大的函数一样,您将X放入,然后得到Y。那太好了。HTML就是一个例子,输入是文本,输出是您在浏览器中看到的内容。


2
我不买这个。为什么您的示例没有声明性?是if/ else,在这种情况下,声明性替代是什么样子?当然不是read/ write/ call部件,因为它们是值的声明性列表(如果您暗示将它们包裹在中{...; ...},为什么不暗示它们被包裹在其中[..., ...]?)当然,列表只是自由的类动物;许多其他人也会这样做。我不明白为什么单子与这里有关。它们只是一个API。Haskell从流-> monad开始帮助进行人工编写,但是逻辑语言可以自动按顺序编写列表。
Warbo 2015年

2
-1代表单子。1.它们并不是真正的异国情调(列表和集合是Monad,每个人都使用它们)。2.他们什么都没有做强制的东西在一个特定的顺序来完成(Haskell的记号的外观必然的,而且是不)。声明/功能语言指定关系和依赖关系。如果函数X需要输入Y,则将在X之前生成Y。正确获取依赖项,并将定义正确的事件序列。很多交互都是事件驱动的,而不是按固定顺序进行的。声明性语言不会使对事件的响应更加困难。
itsbruce 2015年

懒惰使其中的一些问题变得复杂,但是懒惰并不是声明性语言定义的一部分,大多数声明不使用它。在这样做的情况下,保证评估的方式与单子无关。对于其中声明性语言仅用于交互而不是抽象计算的示例,它没有指定顺序,但必须确保顺序发生正确的事情,因此,Puppet DSL别无所求。这样做的好处是只有必要的事情才会发生-某些命令式语言不会使事情变得容易。
itsbruce 2015年

1
除了@itsbruce示例之外,反应式编程还被认为是声明性的,并且与环境相互作用。
Maciej Piechotka 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.