什么是函数式、声明式和命令式编程?
什么是函数式、声明式和命令式编程?
由于每个子表达式在概念上都是一个函数调用,所以RT要求函数的实现(即调用函数内部的表达式)不得访问外部的可变状态(允许访问可变的本地状态)。简而言之,函数(实现)应该是纯的。 纯函数的定义 纯函数通常被认为是“无副作用”的。术语“效果”没有精确定义,因此有些人不同意这个说法。如果对于所有程序
p
,表达式e
在p
中的每个出现都可以替换为评估e
的结果,而不影响p
的可观察结果,则表达式e
是引用透明的。
<p>
在段落中的子表达式(即标签)被评估之前无法显示。没有可变状态,只有由标记层次结构的逻辑关系(子表达式嵌套,类比嵌套函数调用)引起的顺序依赖性。f(g(a,b),h(c,d))
,如果函数f
,g
和h
是纯函数,则函数参数的急切和惰性求值将产生相同的结果。f
,g
和h
不是纯函数,则评估顺序的选择可能会导致不同的结果。f(g a b)(h c d)
。IO
单子可能是不纯的,因此在程序外部的World
状态方面具有命令式的特点(但在下面的警告意义上,副作用是隔离的)。if
,for
,while
)也可以用函数建模。这些“参数”函数的求值顺序(不要与嵌套函数调用顺序混淆)在语法中未声明,例如f( g() )
可以急切地评估g
然后对g
的结果进行f
的评估,或者只有在f
内部需要g
的结果时才会惰性地评估g
。但是函数式编程并不仅限于声明式编程。函数式组合可以与子类型进行对比,特别是在表达式问题方面,其中扩展可以通过添加子类型或者函数分解来实现。扩展可以是两种方法的混合。
函数式编程通常将函数作为一等对象,这意味着函数类型可以出现在语法中的任何其他类型可以出现的地方。其结果是函数可以输入和操作函数,从而通过强调函数组合来实现关注点分离,即将确定性计算的子计算之间的依赖关系分离。
例如,函数式编程不是为每个集合元素应用可能的无限个特定操作而编写单独的函数(如果函数还必须是声明式的,则使用递归而不是循环),而是使用可重用的迭代函数,例如map、fold、filter。这些迭代函数输入第一类专业动作函数。这些迭代函数遍历集合并为每个元素调用输入的专业动作函数。由于这些动作函数不再需要包含循环语句来迭代集合,因此它们更加简洁。这个具有一等函数的功能组合可以通过分离独立函数来表达并行深度。
Brent原则:具有工作量w和深度d的计算可以在p处理器PRAM中以时间O(max(w/p,d))实现。
并发和并行也需要声明式编程,即不可变性和RT。
那么并行性=并发性的这种危险假设从哪里来?这是具有副作用的语言的自然结果:当你的语言到处都有副作用时,每次尝试同时执行多个操作时,你基本上都会出现由每个操作的效果交错引起的非确定性。因此,在具有副作用的语言中,获得并行性的唯一方法是并发性;因此,我们经常看到这两个概念被混淆是不足为奇的。
这种不必要的工作是导致eager和lazy在纯函数时间复杂度方面存在“高达”额外log n因子差异的原因。解决方法是使用functor(例如列表)和具有可选惰性乘积的lazy constructors(即eager),因为对于eager而言,错误的渴望起源于内部函数。这是因为乘积是构造类型,即在初始不动点上具有初始代数的归纳类型[11]
然而,使用惰性时双方都终止。因此,在连词产品中,热切过于热切,并且在不必要的情况下不会终止(包括运行时异常)。
对于1
“或”2
的析取,如果f
不终止,则List(f?1:2,3).tail ==(f?List(1,3):List(2,3)).tail
不成立,因为左侧终止,右侧不终止。
然而,使用渴望时,两边都不终止,因此永远不会达到相等测试。因此,惰性对于分离余积来说太懒了,在这些情况下,它无法终止(包括运行时异常),而比渴望做更多的工作。
[10] Filinski的声明性延续和范畴对偶,第2.5.4节CBV和CBN的比较,以及第3.6.1节SCL中的CBV和CBN。
【2023-03-17】[11] Filinski的《声明性续延和范畴对偶》,第2.2.1节产品和余积,第2.2.2节终点和起点对象,第2.5.2节采用惰性乘积的CBV,以及第2.5.3节采用急切余积的CBN。
对于这些词汇,实际上没有非歧义、客观的定义。以下是我个人的定义:
命令式 - 着重于计算机应该执行哪些步骤,而不是计算机将做什么(例如C、C++、Java)。
声明式 - 着重于计算机应该做什么,而不是如何做到(例如SQL)。
函数式 - 是声明性语言的一个子集,强调递归。
指令式和声明式是两种相反的编程风格。指令式是传统的“逐步配方”方法,而声明式更多地是“这就是我想要的结果,你自己想出如何做到它”。
这两种方法在整个编程中都会出现-即使是使用相同语言和相同程序也会如此。一般来说,声明式方法被认为更可取,因为它使程序员不必指定太多细节,同时具有更少的错误机会(如果您描述所需的结果,并且某些经过充分测试的自动处理程序可以从该结果向后工作以定义步骤,则可能希望事情更可靠,而不必手动指定每个步骤)。
另一方面,指令式方法给您更多的低级控制-它是编程的“微管理者方法”。这样可以让程序员利用关于问题的知识提供更有效的答案。因此,在某些部分以较声明式的风格编写程序是很常见的,但是对于速度关键部分则更倾向于采用较指令式的方法。
正如您所想象的那样,您用于编写程序的语言会影响您可以声明性的程度-具有内置的“智能”以在给定结果描述的情况下工作的语言将允许比需要程序员首先使用指令式代码添加这种类型智能才能构建更声明性层的语言更声明性的方法。例如,像Prolog这样的语言被认为非常声明性,因为它内置了一个搜索答案的过程。
到目前为止,您会注意到我没有提到函数式编程。那是因为它是一个意义与其他两个不直接相关的术语。在其最简单的形式下,函数式编程意味着您使用函数。特别地,您使用支持函数作为“一等值”的语言-这意味着您不仅可以编写函数,还可以编写编写函数的函数(编写函数的函数....),并将函数传递给函数。简而言之,函数像字符串和数字等东西一样灵活而常见。
然而,与此相反的是,函数式编程、命令式编程和声明式编程经常被一起提到。这是因为将函数式编程的思想“极致化”所导致的结果。在其最纯粹的意义上,函数是来自数学的——一种类似于“黑匣子”的东西,它接收一些输入并始终给出相同的输出。这种行为不需要存储可变变量。因此,如果你设计一种编程语言,旨在实现非常纯净、受数学影响的函数式编程思想,你最终会在很大程度上拒绝可变值的概念(以某种有限的技术意义上)。
如果你这样做——限制变量如何改变——那么差不多就会无意中迫使程序员编写更多的声明式代码,因为命令式编程的一个很大部分是描述变量如何改变,而你不能再这样做了!因此,事实证明,函数式编程——特别是在函数式语言中编程——往往会产生更多的声明式代码。
总之:
命令式编程和声明式编程是两种相反的编程风格(鼓励这些风格的编程语言使用相同的名称)。
函数式编程是一种编程风格,其中函数变得非常重要,结果可变值的重要性减弱了。有限的指定值更改的能力迫使采用更多声明式风格。
因此,“函数式编程”经常被描述为“声明式”。
简而言之:
命令式语言 指定计算机按顺序执行的一系列指令(做这个,然后做那个)。
声明式语言 声明了关于哪些输入应该产生哪些输出的一组规则(例如,如果你有A,则结果是B)。引擎将这些规则应用于输入,并给出输出。
函数式语言 声明了一组数学/逻辑函数,定义了如何将输入转换为输出。例如,f(y) = y * y。它是声明式语言的一种类型。
指令式编程: 如何 实现我们的目标
Take the next customer from a list.
If the customer lives in Spain, show their details.
If there are more customers in the list, go to the beginning
声明式:我们想要实现的是什么
Show customer details of every customer living in Spain
命令式编程指的是任何一种程序设计风格,其中你的程序由描述计算机操作方式的指令构成。
声明式编程指的是任何一种程序设计风格,其中你的程序是问题或解决方案的描述,但不明确说明如何完成工作。
函数式编程通过求值函数和函数的函数进行编程... 严格定义的函数式编程意味着通过定义无副作用的数学函数进行编程,因此它是声明式编程的一种形式,但并不是唯一的声明式编程形式。
逻辑编程(例如 Prolog)是另一种声明式编程形式。它通过决定逻辑语句是否为真(或是否可以满足)来计算。程序通常是一系列事实和规则 - 即一种描述而不是指令序列。
术语重写(例如 CASL)是另一种声明式编程形式。它涉及对代数术语进行符号转换。它与逻辑编程和函数式编程完全不同。
命令式表达式描述需要执行的一系列操作(关联)
声明式表达式是对程序行为做出贡献的声明(关联、交换律、幂等、单调性)
函数式表达式只有一个值作为效果;其语义支持等式推导
Applicative
,名义类型、命名空间、命名字段,以及就操作语义的操作级别而言则是纯函数式编程。编辑:我在Robert Harper的博客上发布了以下评论:
在函数式编程中,变量的取值范围是一个类型。根据人们如何区分函数式和命令式编程,命令式程序中的“可赋值项”也可能有一个类型,这限制了它的可变性。我目前唯一明确理解函数式编程的定义是:a) 将函数作为第一类对象和类型;b) 喜欢使用递归而非循环;c) 纯函数——即这些函数不会在记忆化时影响程序所需的语义(因此完全纯的函数式编程不存在于通用指称语义中,由于操作语义,如内存分配的影响)。纯函数的幂等性质意味着可以将其变量上的函数调用替换为其值,但对于命令式过程的参数来说通常情况下并非如此。相对于输入和结果类型之间未组合状态转移,纯函数似乎是声明式的。但是纯函数的组合没有保持任何此类一致性,因为可以在纯函数式编程语言中模拟具有副作用(全局状态)的命令式过程,例如Haskell的IOMonad,而且在任何图灵完备的纯函数式编程语言中都无法防止进行这样做。因此,我得出结论,只有非图灵完备的语言才能是声明式的。因此,一个明确而独特的声明式语言属性可以是其输出可以被证明服从某个可枚举的生成规则集合。例如,对于任何未编写脚本(即非图灵完备)的特定HTML程序(忽略解释器发散的方式的不同),其输出变化范围是可枚举的。更简洁地说,一个HTML程序是其变量可变性的纯函数。电子表格程序也是其输入变量的纯函数。因此,对我来说,声明式语言是 无限递归的对立面,即按照哥德尔的第二不完备定理,自指定理不能被证明。Lesie Lamport 写了一篇童话故事,讲述了欧几里得如何绕过哥德尔不完备性定理,应用于编程语言上的数学证明,通过类型和逻辑之间的相似之处(柯里-霍华德对应等)。命令式编程:告诉“机器”如何做某事,结果你想要发生的事情就会发生。
声明式编程:告诉“机器”你想要发生什么,让计算机自己去解决如何实现。
function makeWidget(options) {
const element = document.createElement('div');
element.style.backgroundColor = options.bgColor;
element.style.width = options.width;
element.style.height = options.height;
element.textContent = options.txt;
return element;
}
function makeWidget(type, txt) {
return new Element(type, txt);
}
过去使用 命令式/声明式/函数式 方面对通用语言进行分类是不错的,但是现在所有“大型语言”(如Java、Python、Javascript等)都有一些选项(通常是框架),可以用“其他焦点”(通常是命令式)来表达,并且可以表达并行进程、声明性函数、lambda等。
因此,一个好的问题变体是“现在用什么方面来分类框架比较好?”......重要的方面是我们可以标记为“编程风格”......
一个很好的例子来解释。正如你可以在维基百科上阅读有关jQuery的内容,
由其选择器引擎启用的一组jQuery核心功能——DOM元素选择、遍历和操作——创造了一种新的“编程风格”,将算法和DOM数据结构融合在一起
因此,jQuery是一个关注于“新编程风格”的最佳(流行)例子,它不仅是面向对象的,而且是“融合了算法和数据结构”。jQuery有点像电子表格一样反应灵敏,但不是“单元格导向”,而是“基于DOM节点的”... 在这个背景下比较主要的“风格”:
无融合:在所有“大语言”中,任何函数式/声明式/命令式表达式都不会融合数据和算法,除非是一些面向对象编程,这从严格的代数结构角度来看是一种融合。
部分融合:现今所有经典的融合策略都有一些框架将其作为范例... 数据流编程,事件驱动编程(或旧领域特定语言如awk和XSLT)...像使用现代电子表格进行编程一样,它们也是响应式编程风格的示例。
大规模融合:就是“jQuery风格”... jQuery是一个专注于“融合算法和DOM数据结构”的领域特定语言。
PS:其他“查询语言”,如XQuery、SQL(带有PL作为命令式表达式选项)也是数据-算法-融合的例子,但它们是孤立的,没有与其他系统模块的融合...当使用find()
-变量和规范子句时,Spring是另一个很好的融合示例。