声明式vs命令式
例如我们有一个用户列表,用 python 查找手机号为 183 开头的用户,可能会这么写:
def get_users():ret = []for user in users:if user['phone'].startswith('183'):ret.append(user)return ret
这是命令式的作法,给出通向目标的每个指令;而声明式语言则直接描述目标,如 SQL 可能会这么写:
SELECT * FROM users where phone like '183%';
显然,声明式语言对用户更友好,用户可以关心更少的细节。更重要的是:它允许多种底层实现方式,保持目标不变的同时不断优化,如上例中 SQL 的实现既可以遍历所有的用户,也可以使用索引来加速查找。
而命令式的好处自然是它的表达能力了,图灵完备的语言可以表达任何的可计算问题。
声明式不是万能的
声明式语言直接描述目标,那怎么才能清晰地描述目标呢?有时候也需要命令式的帮助。
考虑下面的命令式的伪代码要如何用 SQL 实现:
goods = (SELECT * from goods)for g in goods:#>> 注意在内层循环中可以引用外层表 goods 的字段evaluations = (SELECT * from evaluations e where e.good_id == g.id)if len(evaluations) > 3:print(g, len(evaluations))
会发现使用常规的 JOIN 语义,很难实现上述目标。子查询里是无法引用其它查询的字段的,这本身是一种优势,数据库内部可以对 JOIN 的实现进行优化,但同时也限制了对复杂 JOIN 语义的表达。
后来 SQL 里加了个关键字:LATERAL[3],用来表达子查询的先后顺序,上例可以写成:
SELECT g.*, e.num FROM goods as gLEFT JOIN LATERAL(SELECT COUNT(ev.id) as num FROM evaluation AS ev WHERE ev.goods_id=g.id) AS e ON TRUE WHERE num > 3
有了 LATERAL,在 LATERAL 后的子查询就可以引用前面子查询的变量。那么LATERAL 算是声明式还是命令式?似乎变得模糊了,一方面它依旧是表达目标,是声明式;另一方面它似乎指定了操作步骤(先查 goods,再查 evaluations)属于命令式。
当描述的目标变复杂时,声明式语言也不可避免变得更命令式,通过描述过程来描述更多细节
命令式里的声明式
传统上的一些编程语言,如 C/C++、Java、Python 等都被认为是命令式语言。用这些语言编写程序时的确是一条语句一条语句导向最终的目标。但这些编程语言与声明式的界限也并非泾渭分明。
除了机器码,包括汇编在内的几乎其它所有编程语言都有“函数”的概念。通过将语句组装成函数,无论是在使用还是阅读上,似乎都可以认为是在指定目标,是声明式的。例如要计算 Fibonacci 数列的第 N 个数,如果已经有现成的库,我们也只需要写 x = fibonacci(n),似乎也不是“命令式”吧。
另外,编程语言的一些语法糖也加强了我们“声明目标”的能力,如 Python 的装饰器 (decorator) @dataclass,“声明”式地定义一个类为数据类,Java 的 lombok 库也有 @Data 这样的注解(annotation)实现类似的功能。
通过适当的封装、组件化,命令式也可以变成目标导向,变得更加“声明式”
小结
声明式使用方便、容易理解、易于优化,但表达能力有限,要表达更复杂的目标时,它往往也在向命令式靠拢了。而命令式里很多重复性的工作,也可以通过适当地组件化部分变成声明式。这样看来,一门语言是声明式还是命令式,似乎取决于我们接触的细节多少。
生活中,大老板决定路线,小老板决定方案,螺丝钉具体落实,不也类似嘛。
在我们设计语言、库时,尽可能地将接口设计得“声明式”,暴露更少的细节给用户,不仅能让用户用得开心,也方便内部的扩展、优化。
参考
/ React 的设计理念:Declarative view
《Design Data-Intensive Applications》第二章,说明 CSS/XSL 是声明式的语言
未来属于声明式编程 对声明式编程语言的思考
命令式和声明式,哪个才是你的菜 描述了声明式、命令式的一些差别
引用
[1] /wiki/Declarative_programming
[2] /wiki/Imperative_programming
[3] 出现在 ISO/IEC 7095:199 标准中,Postgre、 Flink 支持