声明式编程是什么?

193

我听到这个术语在几个不同的背景下被提起。它是什么意思?


7
您所选择的正确答案(绿色勾号标识)是错误的。它没有定义什么区分了声明式编程和它的反义词——命令式编程。请考虑更改您的选择。 - Shelby Moore III
3
是的,标记为正确答案的回答是不正确的。 - Dermot
4
@ShelbyMooreIII 请同时指明哪个答案是正确的,以便我们阅读! - vivek.m
@vivek.m 今天我提供了一个新的答案 - Shelby Moore III
18个回答

148

声明式编程是指以描述想要实现的功能为主,而非如何实现为主的代码编写方式。具体的实现方法由编译器确定。

声明式编程语言的例子包括SQL和Prolog。


29
你仍需要弄清楚如何告诉计算机你想要什么,即使是“如何”告诉它。 :) - hasen
7
@hasenj和其他回答没有定义唯一一个与命令式编程不共享的属性——它是不可变性 - Shelby Moore III
3
如果您能说明它与命令式编程有何不同(例如C、C ++、C#等语言),那将非常好,这样读者就更容易理解它们之间的区别。 - RBT
1
程序员: "我想去巴黎旅行。" 声明式(c): "你想怎么去?坐船?还是坐飞机?或者,先坐船到一半,然后再坐飞机到那里?" 程序员: "我对如何实现没有兴趣。" 命令式(sql): "别担心。我可以查询你需要的内容。" - nate
如果SQL支持不具有引用透明性的表达式,它如何能够声明式呢? - java-addict301

80

其他回答已经很好地解释了什么是声明式编程,所以我将提供一些为什么这可能有用的例子。

上下文无关性

声明式程序是上下文无关的。因为它们只声明最终目标,而不声明达到该目标的中间步骤,所以同一个程序可以在不同的上下文中使用。这对于命令式程序来说很难做到,因为它们通常依赖于上下文(例如隐藏状态)。

yacc为例。它是一个解析器生成器,也就是编译器编译器,是一种外部声明式DSL,用于描述语言的语法,从而可以从描述中自动生成该语言的解析器。由于其上下文无关性,您可以使用这样的语法做许多不同的事情:

  • 为该语法生成C解析器(yacc的原始用例)
  • 为该语法生成C++解析器
  • 为该语法生成Java解析器(使用Jay)
  • 为该语法生成C#解析器(使用GPPG)
  • 为该语法生成Ruby解析器(使用Racc)
  • 为该语法生成树形可视化(使用GraphViz)
  • 仅对yacc源文件进行一些漂亮的打印、格式化和语法高亮处理,并将其包含在您的参考手册中,作为您的语言的语法规范。

还有许多其他用途…

优化

因为你不指定计算机按什么顺序执行,所以它可以更自由地重新排列你的程序,甚至可以并行执行一些任务。一个很好的例子是 SQL 数据库的查询规划器和查询优化器。大多数 SQL 数据库允许您显示它们实际执行的查询与您要求执行的查询的区别。通常,这两个查询看起来完全不同。查询规划器会考虑到你从未想过的事情:例如磁盘盘片的旋转延迟,或者某个完全不同的用户的完全不同应用程序刚刚执行了一个类似的查询,而你正在连接的表已经在内存中了,这也是你努力避免加载的表。
这里有一个有趣的折衷:机器必须更加努力地找出如何做某件事情,比起命令式语言来说,但是当它找到答案时,它拥有更多的自由度和更多的优化信息。

23

简而言之:

声明式编程倾向于:

  • 一组声明或声明语句,每个声明都具有意义(通常在问题领域内),可以独立且独立理解。

命令式编程倾向于:

  • 一系列命令,每个命令执行某些操作;但这些操作可能与问题领域没有关联。

因此,命令式风格帮助读者理解系统实际执行的机制,但可能很少揭示它的目的。另一方面,声明式风格有助于读者理解问题领域和系统解决问题的方法,但在机制方面的信息较少。

真正的程序(即使是使用偏向于某一端的语言编写的程序,如ProLog或C),在不同的点上以不同程度地同时具备这两种风格,以满足作品的各种复杂性和沟通需求。一种风格不比另一种更优越;它们只是服务于不同的目的,就像生活中的许多事情一样,适度才是关键。


那是正确的答案。不是上面那些含糊不清的话。 - Krzysztof Wende
谢谢你不仅回答了问题,而且用上下文和实用性的“像我五岁一样解释”的方式来回答。非常好的答案。 - monsto

17

这里有一个例子。

在用于样式化HTML页面的CSS中,如果您想要一个图像元素高度为100像素,宽度为100像素,您只需"声明"您想要的大小,如下所示:

#myImageId {
height: 100px;
width: 100px;
}
你可以把 CSS 视为一种声明性的“样式表”语言。 解释这个 CSS 的浏览器引擎可以自由地使图像以任何它想要的高度和宽度显示。不同的浏览器引擎(例如,IE 的引擎、Chrome 的引擎)会以不同的方式实现此任务。 当然,它们独特的实现并不是用声明性语言编写的,而是用程序化语言,如 Assembly、C、C++、Java、JavaScript 或 Python 编写的。那些代码是一堆要一步一步执行的步骤(可能包括函数调用)。它可能做像插值像素值和渲染到屏幕上这样的事情。

11

很抱歉,我必须反对其他答案中的许多观点。我希望停止混淆声明式编程定义的误解。

定义

参考透明度(RT)是声明式编程表达式的唯一必需属性,因为它是与命令式编程不共享的唯一属性。

其他引用的声明性编程属性来源于此参考透明度。请单击上面的超链接以获取详细说明。

电子表格示例

有两个答案提到了电子表格编程。在电子表格编程(即公式)不访问可变全局状态的情况下,则属于声明式编程。这是因为可变单元格值是main()(整个程序)的单一输入和输出。在执行电子表格中的所有公式期间,新值不会在每次执行公式后写入单元格,因此它们在声明式程序的生命周期内不可变(执行电子表格中的所有公式)。因此,相对于彼此,这些公式将这些可变单元格视为不可变的。RT函数可以访问不可变的全局状态(以及可变的本地状态)。

因此,当程序终止时可以更改单元格中的值(作为main()的输出),但这不会使它们成为上下文中可变的存储值。关键区别在于单元格值在执行每个电子表格公式后未更新,因此执行公式的顺序无关紧要。只有在所有声明性公式执行完毕后,才会更新单元格值。


1
意外的副作用可能会破坏程序声明与实际行为之间的关系。我在一个新的回答里更详细地解释了这一点(https://dev59.com/DHRB5IYBdhLWcg3wgXdV#15382180)。 - Shelby Moore III

8
声明式编程是画面,而命令式编程则是绘制这张画需要的指令。
如果你在“告诉计算机它是什么”,而不是描述计算机应该采取哪些步骤来达到你想要的目标,则你正在使用声明式风格进行编写。
当你使用XML标记数据时,你正在使用声明式编程,因为你在说“这是一个人,那是一个生日,那边是街道地址”。
一些将声明式和命令式编程结合起来以获得更大效果的例子:
- Windows Presentation Foundation使用声明式XML语法来描述用户界面的外观以及控件和基础数据结构之间的关系(绑定)。 - 结构化配置文件使用声明式语法(简单地说就是“键=值”对)来标识字符串或数据值的含义。 - HTML使用标签标记文本,描述每个文本片段与整个文档的关系。

2
尽管XML具有声明性,但我不会说它仅仅是声明性“编程”,因为标记没有关联活动语义。仅仅说某个元素是地址并不能帮助您找出要对其执行什么操作。 - HenryR
1
声明式程序必须在某个基础上下文(领域?)中使用。因此,将XML与ANT结合使用可以被解释为声明式程序。 - Gutzofter

7
声明式编程是使用声明句子进行编程。声明句子具有许多特性,可以将它们与命令句子区分开来。特别是,声明具有以下特点:
  • 可交换(可以重新排序)
  • 可结合(可以重新分组)
  • 幂等(可以重复而不改变含义)
  • 单调(声明不会减少信息)
一个相关的观点是,这些都是结构特性,与主题无关。声明式编程不是关于“什么 vs. 如何”。我们可以像声明“什么”一样轻松地声明(表示和约束)一个“如何”。声明式编程对我们如何抽象和重构代码以及如何将其模块化为子程序产生了重大影响,但对领域模型则不太做出影响。
通常,我们可以通过添加上下文将命令式转换为声明式。例如从“左转。(...等待...)右转。”到“Bob将在Foo和Bar交叉口处于11:01左转。Bob将在Bar和Baz交叉口处于11:06右转。“请注意,在后一种情况下,句子是幂等且可交换的,而在前一种情况下,重新排列或重复句子会严重改变程序的含义。
关于单调性,声明可以添加约束,从而减少可能性。但是约束仍然添加信息(更确切地说,约束是信息)。如果我们需要具有时变声明,通常会使用显式的时间语义来建模 - 例如从“球是扁的”到“球在时间T是扁的”。如果我们有两个相互矛盾的声明,则具有不一致声明系统,尽管这可以通过引入软约束(优先级,概率等)或利用可超前逻辑来解决。

1
声明式表达式有助于程序的预期行为,而命令式则可能对预期或非预期行为产生影响。如果这是有意义的语义,声明式不需要是可交换和幂等的。 - Shelby Moore III

6

向计算机描述您想要的内容,而不是如何做某件事。


6
想象一个Excel页面。其中的列填充有计算税收退回的公式。
所有逻辑都在单元格中声明,计算顺序是由公式本身确定的,而不是程序上的顺序。
这就是声明式编程的基础。你声明问题空间和解决方案,而不是程序流程。
Prolog是我使用过的唯一声明式语言。它需要不同的思考方式,但学习它对于暴露你于其他典型的过程式编程语言之外的东西是有好处的。

6

自从2011年12月我提供了一个答案以来,我已经完善了对声明式编程的理解。以下是我的当前理解。

我的理解(研究)的详细版本在这个链接中详细说明,您应该阅读它以深入了解我将在下面提供的摘要。

命令式编程是存储和读取可变状态的地方,因此程序指令的排序和/或复制可以改变程序的行为(语义),甚至可能导致错误(即意外行为)。

在最天真和极端的意义上(我在之前的答案中断言过),声明式编程(DP)避免所有存储的可变状态,因此程序指令的排序和/或复制不能改变程序的行为(语义)。

然而,在现实世界中,这样极端的定义并不是很有用,因为几乎每个程序都涉及存储的可变状态。 电子表格示例 符合DP的这种极端定义,因为整个程序代码在使用一个静态副本的输入状态完成后运行到完成,然后存储新状态。然后,如果更改任何状态,则会重复此过程。但大多数现实世界的程序无法局限于这样的状态更改单一模型。
DP的更有用的定义是,编程指令的排序和/或复制不会改变任何不透明语义。换句话说,没有发生任何隐含的随机语义更改 - 程序指令顺序和/或复制的任何更改只会导致预期和透明的程序行为更改。
下一步将是讨论哪些编程模型或范例有助于DP,但这不是本问题的关键。

更新:请参考我在此答案中对声明式编程定义的更详尽解释。 - Shelby Moore III
函数式编程是当今的热词,本质上是声明式编程的子集。在C#语言中,LINQ是函数式编程的一个元素,尽管该语言本质上是命令式的。因此,根据这个定义,C#变成了一种混合型语言。 - RBT
1
compute.com的链接已经失效。 - Kedar Mhaswade

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接