声明式与命令式:编程世界的两种思路
《声明式与命令式:编程世界的两种思路》
声明式和命令式是编程领域中两个重要的概念。声明式是结果导向的,就像我们朝着一个目的地前行,重点是那个要到达的地方;而命令式是过程导向的,更关注通向目标的每一步。这两种方式各有适用场景与局限,所以在现实中的编程语言里,常常能看到它们二者的影子。
先来说说命令式和声明式的定义。声明式编程是一种编程范式,它表达计算的逻辑时并不描述其控制流。而命令式编程这种范式呢,是通过语句来改变程序的状态。
举个例子,假设有一个用户列表,如果用 Python 查找手机号以 183 开头的用户,代码可能是这样:
1 | |
这就是命令式的做法,把达到目标的每一个指令都列出来。但如果是声明式语言,就会直接描述目标,比如 SQL 语句可能是:
1 | |
很明显,声明式语言对用户更友好,用户不用操心太多细节。更关键的是,它允许多种底层实现方式,在目标不变的情况下能持续优化。就像上面 SQL 的例子,既可以遍历所有用户来查找,也可以利用索引加速查找。
不过,命令式也有它的好处,那就是强大的表达能力。图灵完备的语言能表达任何可计算问题呢。
声明式也不是无所不能的。因为声明式语言是直接描述目标的,那要是目标不好清晰描述呢?这时候就需要命令式来帮忙了。
比如下面这个命令式的伪代码,要想用 SQL 实现就有点麻烦:
1 | |
会发现用常规的 JOIN 语义很难做到。子查询里不能引用其他查询的字段,这本来是个优势,数据库内部能对 JOIN 实现优化,但也限制了对复杂 JOIN 语义的表达。后来 SQL 里加了个关键字 LATERAL,用它可以表达子查询的先后顺序,上面的例子就可以写成:
1 | |
有了 LATERAL,在它后面的子查询就能引用前面子查询的变量了。那 LATERAL 算是声明式还是命令式呢?好像有点模糊了。从一方面看,它还是在表达目标,是声明式;但从另一方面看,它好像指定了操作步骤(先查 goods,再查 evaluations),又像是命令式。当描述的目标变得复杂时,声明式语言也难免会更偏向命令式,得通过描述过程来呈现更多细节。
在传统的编程语言里,像 C/C++、Java、Python 等,一般都被认为是命令式语言。用这些语言写程序确实是一条语句一条语句地朝着最终目标前进。但这些编程语言和声明式的界限也不是那么绝对的。
除了机器码,几乎其他所有编程语言包括汇编都有“函数”这个概念。把语句组装成函数,不管是使用还是阅读的时候,都可以看成是在指定目标,有声明式的感觉。比如要计算 Fibonacci 数列的第 N 个数,如果有现成的库,我们只要写 x = fibonacci(n),这好像就不太像“命令式”了。
而且,编程语言的一些语法糖也增强了我们“声明目标”的能力。像 Python 的装饰器 @dataclass,可以“声明”式地把一个类定义成数据类,Java 的 lombok 库中的 @Data 注解也有类似功能。通过适当的封装和组件化,命令式也能朝着目标导向发展,变得更“声明式”。
总结一下,声明式用起来方便、好理解,也容易优化,不过表达能力有限,当要表达更复杂目标的时候,往往会向命令式靠近。而命令式里很多重复性工作,通过适当组件化,部分也能变成声明式。这么看来,一门语言是声明式还是命令式,好像取决于我们接触到的细节多少。
这就像使用空调一样,我们只需要通过遥控器设定想要的温度(目标),并不需要关心空调内部是如何通过压缩机、风扇等部件的协同工作(过程)来实现温度调节的。在设计语言、库的时候,我们也应该尽量把接口设计得 “声明式”,少向用户暴露细节,这样用户使用起来轻松便捷,同时也有利于内部的扩展和优化。