为什么没有实现声明式编程的梦想?有什么具体的障碍阻碍?举个简单的例子,为什么我不能说
sort(A) is defined by sort(A) in perm(A) && asc(sort(A))
并自动获得排序算法。perm
表示排列,asc
表示上升。
n!
一个序列的置换,在最坏的情况下,您的算法将不得不尝试全部序列以找到排序的序列。阶乘时间与处理序列的算法一样糟糕。
为什么没有实现声明式编程的梦想?有什么具体的障碍阻碍?举个简单的例子,为什么我不能说
sort(A) is defined by sort(A) in perm(A) && asc(sort(A))
并自动获得排序算法。perm
表示排列,asc
表示上升。
n!
一个序列的置换,在最坏的情况下,您的算法将不得不尝试全部序列以找到排序的序列。阶乘时间与处理序列的算法一样糟糕。
Answers:
有一些很好的答案。我将尽力为讨论做出贡献。
关于Prolog中的声明性逻辑编程主题,理查德·奥基夫(Richard O'Keefe)撰写了一本很棒的书“ Prolog of Prolog”。它是关于使用一种编程语言来编写高效的程序,该语言可以让您编写效率很低的程序。在本书中,在讨论几种算法的有效实现时(在“编程方法”一章中),作者采用了以下方法:
通过这些工作,我能够做出的最启发性的发现(对我而言):
是的,实现的最终版本比作者开始使用的“声明性”规范有效得多。它仍然是非常声明,简洁和易于理解的。介于两者之间的是最终解决方案捕获了最初解决方案所忽略的问题的属性。
换句话说,在实施解决方案时,我们会尽可能多地使用我们对问题的了解。比较:
查找列表的排列,以使所有元素都按升序排列
至:
合并两个排序列表将产生一个排序列表。由于可能已经排序了子列表,因此请使用这些列表作为起点,而不要使用长度为1的子列表。
除了一点点:像您给出的定义一样有吸引力,因为它非常笼统。但是,我无法避免这样一种感觉,即它有目的地忽略了排列是组合问题的事实。这是我们已经知道的!这不是批评,而只是观察。
至于真正的问题:如何前进?嗯,一种方法是提供有关我们要向计算机声明的问题的尽可能多的知识。
我真正解决问题的最佳尝试是在亚历山大·斯蒂芬诺夫(Alexander Stepanov)合着的《编程要素》和《从数学到泛型编程》中提出的。不幸的是,我没有完成总结(甚至完全理解)这些书中所有内容的任务。然而,在事先知道输入的所有相关属性的前提下,该方法将定义有效(甚至最佳)的库算法和数据结构。最终结果是:
至于为什么它还没有发生,那么,计算机科学是一个非常年轻的领域,我们仍在努力真正地欣赏其中大多数的新颖性。
聚苯乙烯
让您了解“完善实现”的含义:例如在Prolog中获取列表最后一个元素的简单问题。规范的声明式解决方案是说:
last(List, Last) :-
append(_, [Last], List).
在此,声明性含义append/3
是:
List1AndList2
是的串联List1
和List2
由于在第二个参数中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 Overflow的Prolog标记中进行一些问答。
逻辑语言已经做到了。您可以像执行操作一样类似地定义排序。
主要问题是性能。计算机可能擅长计算很多东西,但是它们本质上是愚蠢的。程序员可能将计算机可能做出的每个“明智”决定都编程了进去。通常,不是通过最终结果的样子来描述此决定,而是通过逐步实现此最终结果的方式来描述。
想象一下魔像的故事。如果您尝试给他一个抽象的命令,那么充其量是,他将效率低下,最糟糕的是,它将伤害自己,您或其他人。但是,如果您尽可能详细地描述您想要的内容,则可以保证任务将有效有效地完成。
决定使用哪种抽象级别是程序员的工作。对于您正在制作的应用程序,您是否要进行高层描述并以抽象的方式对其进行描述,使性能受到影响或变得低落,肮脏,花费10倍以上的时间,但获得的性能要高1000倍?
除了Euphoric的优点之外,我想补充一点,我们已经在许多运行良好的地方使用了声明性语言,即描述状态不太可能改变或请求计算机实际上可以为其生成有效代码的内容在其自己的:
HTML声明了网页的内容。
CSS声明网页中各种元素的外观。
每个关系数据库都有一种数据定义语言,用于声明数据库的结构。
SQL更接近于声明式,而不是命令式,因为您告诉了您想看的内容,并且数据库的查询计划器确定了如何使其真正实现。
有人可能会说大多数配置文件(.vimrc,.profile,.bashrc,.gitconfig)使用的是特定于域的语言,该语言在很大程度上是声明性的
您可以在声明性系统中声明所需内容,然后编译器或解释器会确定执行顺序。从理论上讲,它的好处是使您不必考虑“如何做”,而不必详细说明此实现。但是,在实践中,对于通用计算,您仍然必须考虑“如何”并编写各种技巧,同时牢记如何实现,因为否则编译器可以(并且经常会)选择将要实现的实现。非常非常非常慢(例如n!个操作,其中n就足够了)。
在您的特定示例中,您将获得A排序算法-这并不意味着您将获得一个好甚至是有点可用的算法。您给定的定义,如果按字面意义实现(可能会像编译器一样),则会导致http://en.wikipedia.org/wiki/Bogosort不能用于较大的数据集-从技术上讲是正确的,但需要永恒才能对一千个数字进行排序。
对于某些有限的域,您可以编写在确定良好实现方面几乎总是表现出色的系统,例如SQL。对于效果不佳的通用计算-您可以在Prolog中编写系统,但是您必须可视化最终将如何将声明准确转换为命令式执行顺序,并且会损失很多预期的声明式编程的好处。
计算可确定性是声明性编程尚未被证明像看起来那样容易的最重要原因。
事实证明,许多相对容易陈述的问题是无法决定的,或者需要解决NP完全复杂的问题。当我们考虑否定类和分类,可数性和递归时,通常会发生这种情况。
我想用一些众所周知的领域来体现这一点。
决定使用哪个CSS类需要知识和对所有CSS规则的考虑。添加新规则可能会使所有其他决定无效。由于NP完全问题,有意不将负CSS类添加到语言中,但是缺少负类会使CSS设计决策复杂化。
在(SQL)查询优化器中,存在一个棘手的问题,即决定以哪种顺序进行连接,使用哪些索引以及将哪些内存分配给哪些临时结果。这是一个已知的NP完全问题,使数据库设计和查询制定变得复杂。用不同的方式表述:在设计数据库或查询时,设计人员需要知道查询优化器可能采取的操作和操作顺序。经验丰富的工程师需要了解主要数据库供应商使用的启发式技术。
配置文件是声明性的,但是某些配置很难声明。例如,要正确配置功能,需要考虑版本控制,部署(和部署历史记录),可能的手动替代以及与其他设置的冲突。正确验证配置可能会成为NP完全问题。
结果是这些复杂性使初学者感到惊讶,它们破坏了声明式编程的“美”,并导致一些工程师寻找其他解决方案。没有经验的工程师从SQL迁移到NoSQL可能是关系数据库的潜在复杂性触发的。
我们在编程语言的声明性方面存在差异,可以很好地用于数字逻辑的验证。
通常,数字逻辑以寄存器传输级(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自动检查其是否一致。
通常,问题在于如何对数据建模;声明式编程在这里没有帮助。在命令式语言中,您已经拥有大量的库,可以为您做很多事情,因此您只需要知道要调用的内容。在具体的方式可以考虑这个声明性编程(可能是最好的例子是流API中的Java 8)。有了这个,抽象就已经解决了,不需要声明式编程。
而且,正如已经说过的那样,逻辑编程语言已经完全可以满足您的要求。也许有人会说问题出在性能上,但是通过当今该领域的硬件和研究,可以改进一切以准备投入生产。Prolog实际上是用于AI的东西,但我相信只有学术界才能使用。
要注意的是,它适用于通用编程语言。对于特定领域的语言,声明性语言会更好。SQL可能是最好的例子。
看起来像这样。。{(无论如何=>读取文件并调用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多个错误。关于这一点,还有很多要说的,但是它有效。如果汽车制造商使用此方法检查固件,则不会看到我们现在看到的故障。
并非所有内容都能以声明方式表示。
通常,您明确地想控制执行流程
例如下面的伪代码:
if whatever
read a file
call an URL
else
call an URL
write a file
您将如何声明性地表示它?
当然,可能有一些特殊的方法可以做到这一点。像单子。但是这些通常比它们的过程部分更麻烦,更复杂并且不那么直观。
归结为以下事实:与您的环境/系统“交互” 不是声明性的。本质上,与I / O相关的所有过程都是过程性的。您必须准确说明何时,什么情况以及应按什么顺序进行。
声明式对于纯粹与计算有关的所有事情都非常有用。就像一个巨大的函数一样,您将X放入,然后得到Y。那太好了。HTML就是一个例子,输入是文本,输出是您在浏览器中看到的内容。
if
/ else
,在这种情况下,声明性替代是什么样子?当然不是read
/ write
/ call
部件,因为它们是值的声明性列表(如果您暗示将它们包裹在中{...; ...}
,为什么不暗示它们被包裹在其中[..., ...]
?)当然,列表只是自由的类动物;许多其他人也会这样做。我不明白为什么单子与这里有关。它们只是一个API。Haskell从流-> monad开始帮助进行人工编写,但是逻辑语言可以自动按顺序编写列表。