函数式编程、声明式编程和命令式编程是什么意思?
当前回答
命令式——表达式描述要执行的动作序列(关联的)
声明性——表达式是对程序行为做出贡献的声明(关联、交换、幂等、单调)
函数式表达式的值仅为效果;语义支持等式推理
其他回答
我认为你的分类是不正确的。有两种相反的类型,命令式和声明式。函数式只是声明式的一个子类型。顺便说一句,维基百科也说了同样的事实。
简而言之,一种编程风格越是强调“做什么”(What (to do))而忽略了“如何做”(How (to do))的细节,这种风格就越被认为是声明性的。而imperative则相反。函数式编程与声明式风格相关联。
在写这篇文章的时候,本页上投票最多的答案在声明式和命令式的定义上是不精确的,包括引用维基百科的答案。一些答案是用不同的方式合并了这两个术语。
还可以参考我对电子表格编程为什么是声明性的解释,而不管公式是否会改变单元格。
另外,有几个答案声称函数式编程必须是声明式编程的一个子集。在这一点上,这取决于我们是否区分“功能”和“过程”。让我们先处理命令式和声明式。
声明式表达式的定义
唯一可能区分声明式表达式和命令式表达式的属性是其子表达式的引用透明性(RT)。所有其他属性要么在两种类型的表达式之间共享,要么从RT派生。
100%声明性语言(即每一个可能的表达式都是RT的语言)不允许(在其他RT要求中)对存储值进行突变,例如HTML和大部分Haskell。
RT表达式的定义
RT通常被称为“无副作用”。“效果”这个术语并没有一个精确的定义,所以有些人不同意“无副作用”与RT是一样的。RT有一个精确的定义:
如果对于所有程序p, e在p中的每一次出现都可以替换为计算e的结果,而不影响p的可观察结果,则表达式e是引用透明的。
由于每个子表达式在概念上都是函数调用,RT要求函数的实现(即被调用函数内部的表达式)不能访问函数外部的可变状态(允许访问可变的局部状态)。简单地说,函数(实现)应该是纯的。
纯函数的定义
纯功能常被称为“无副作用”。“效应”这个术语并没有一个精确的定义,所以有些人不同意。
纯函数具有以下属性。
唯一可观察的输出是返回值。 唯一的输出依赖项是参数。 在生成任何输出之前完全确定参数。
记住,RT适用于表达式(包括函数调用),而purity适用于函数的(实现)。
生成RT表达式的不纯函数的一个模糊示例是并发,但这是因为在中断抽象层,纯函数被打破了。你不需要知道这些。要生成RT表达式,需要调用纯函数。
RT的导数属性
为声明式编程引用的任何其他属性,例如维基百科使用的1999年的引用,要么来自RT,要么与命令式编程共享。从而证明我的精确定义是正确的。
注意,外部值的不可变性是RT需求的一个子集。
Declarative languages don't have looping control structures, e.g. for and while, because due to immutability, the loop condition would never change. Declarative languages don't express control-flow other than nested function order (a.k.a logical dependencies), because due to immutability, other choices of evaluation order do not change the result (see below). Declarative languages express logical "steps" (i.e. the nested RT function call order), but whether each function call is a higher level semantic (i.e. "what to do") is not a requirement of declarative programming. The distinction from imperative is that due to immutability (i.e. more generally RT), these "steps" cannot depend on mutable state, rather only the relational order of the expressed logic (i.e. the order of nesting of the function calls, a.k.a. sub-expressions).
例如,HTML段落<p>不能显示,直到该段落中的子表达式(即标签)被求值。没有可变状态,只有由于标记层次结构的逻辑关系而产生的顺序依赖(子表达式的嵌套,类似于嵌套函数调用)。
因此,就有了不可变性(更普遍的是RT)的衍生属性,即声明式表达式只表达组成部分(即子表达式函数参数)的逻辑关系,而不是可变状态关系。
评估顺序
子表达式求值顺序的选择只能在任何函数调用不是RT(即函数不是纯函数)时给出不同的结果,例如在函数内部访问函数外部的某些可变状态。
例如,给定一些嵌套表达式,例如f(g(a, b), h(c, d)),如果函数f、g和h是纯函数,则对函数参数的急切求值和惰性求值将给出相同的结果。
然而,如果函数f, g和h不是纯的,那么求值顺序的选择可以给出不同的结果。
注意,嵌套表达式在概念上是嵌套函数,因为表达式操作符只是伪装成一元前缀、一元后缀或二进制中缀表示法的函数调用。
切题地说,如果所有的标识符,例如a, b, c, d,在任何地方都是不可变的,程序外部的状态不能被访问(即I/O),并且没有抽象层破坏,那么函数总是纯的。
顺便说一下,Haskell有一个不同的语法,f (g a b) (h c d)。
评估订单详情
A function is a state transition (not a mutable stored value) from the input to the output. For RT compositions of calls to pure functions, the order-of-execution of these state transitions is independent. The state transition of each function call is independent of the others, due to lack of side-effects and the principle that an RT function may be replaced by its cached value. To correct a popular misconception, pure monadic composition is always declarative and RT, in spite of the fact that Haskell's IO monad is arguably impure and thus imperative w.r.t. the World state external to the program (but in the sense of the caveat below, the side-effects are isolated).
主动求值意味着函数实参在函数被调用之前被求值,而惰性求值意味着实参在函数内部被访问之前不会被求值。
定义:函数形参在函数定义站点声明,函数实参在函数调用站点提供。知道参数和参数的区别。
Conceptually, all expressions are (a composition of) function calls, e.g. constants are functions without inputs, unary operators are functions with one input, binary infix operators are functions with two inputs, constructors are functions, and even control statements (e.g. if, for, while) can be modeled with functions. The order that these argument functions (do not confuse with nested function call order) are evaluated is not declared by the syntax, e.g. f( g() ) could eagerly evaluate g then f on g's result or it could evaluate f and only lazily evaluate g when its result is needed within f.
注意,没有图灵完备语言(即允许无界递归)是完全声明性的,例如,惰性求值引入了内存和时间不确定性。但是,由于计算顺序的选择而产生的这些副作用仅限于内存消耗、执行时间、延迟、非终止和外部迟滞(即外部同步)。
函数式编程
因为声明式编程不能有循环,所以迭代的唯一方法是函数递归。正是在这个意义上,函数式编程与声明式编程相关。
但是函数式编程并不局限于声明式编程。函数组合可以与子类型相比较,特别是在表达式问题中,扩展可以通过添加子类型或函数分解来实现。扩展可以是两种方法的混合。
函数式编程通常使函数成为一级对象,这意味着函数类型可以出现在语法中任何其他类型可能出现的地方。结果是函数可以输入函数并对函数进行操作,从而通过强调函数组合来提供关注点分离,即分离确定性计算的子计算之间的依赖关系。
For example, instead of writing a separate function (and employing recursion instead of loops if the function must also be declarative) for each of an infinite number of possible specialized actions that could be applied to each element of a collection, functional programming employs reusable iteration functions, e.g. map, fold, filter. These iteration functions input a first-class specialized action function. These iteration functions iterate the collection and call the input specialized action function for each element. These action functions are more concise because they no longer need to contain the looping statements to iterate the collection.
但是,请注意,如果一个函数不是纯函数,那么它实际上是一个过程。我们也许可以认为,使用非纯函数的函数式编程实际上是过程式编程。因此,如果我们同意声明式表达式是RT,那么我们可以说过程式编程不是声明式编程,因此我们可能会认为函数式编程始终是RT,并且必须是声明式编程的子集。
并行性
这种与一类函数的函数组合通过分离出独立函数来表达并行度的深度。
布伦特原理:计算用功w和深度d即可 在时间O(max(w/p, d))的p处理器PRAM中实现。
并发性和并行性都需要声明性编程,即不可变性和RT。
那么,并行性=并发性这个危险的假设在哪里呢 从何而来?这是带有副作用的语言的自然结果: 当你的语言到处都有副作用时,那么任何时候你尝试 一次做不止一件事 由各因素相互作用而引起的不确定性 操作。所以用副作用的语言来说,唯一的方法 并行性是并发性;因此,我们 经常看到两者合并。
FP评估订单
注意,求值顺序还会影响功能组合的终止和性能副作用。
Eager (CBV) and lazy (CBN) are categorical duels[10], because they have reversed evaluation order, i.e. whether the outer or inner functions respectively are evaluated first. Imagine an upside-down tree, then eager evaluates from function tree branch tips up the branch hierarchy to the top-level function trunk; whereas, lazy evaluates from the trunk down to the branch tips. Eager doesn't have conjunctive products ("and", a/k/a categorical "products") and lazy doesn't have disjunctive coproducts ("or", a/k/a categorical "sums")[11].
性能
急切的
与non- terminate一样,eager对于连接功能组合来说过于急切,即组合控制结构做了lazy无法完成的不必要的工作。例如,当由一个终止于第一个真元素的fold组成时,eager急切而不必要地将整个列表映射到布尔值。
这种不必要的工作就是所谓的“高达”log n的连续时间复杂度的原因,它们都是纯函数。一种解决方案是使用函子(例如,列表)和惰性构造函数(例如,eager和可选的惰性乘积),因为使用eager时,eager的错误源于内部函数。这是因为积是构造类型,即在初始固定点[11]上具有初始代数的归纳类型
懒惰的
As with non-termination, lazy is too lazy with disjunctive functional composition, i.e. coinductive finality can occur later than necessary, resulting in both unnecessary work and non-determinism of the lateness that isn't the case with eager[10][11]. Examples of finality are state, timing, non-termination, and runtime exceptions. These are imperative side-effects, but even in a pure declarative language (e.g. Haskell), there is state in the imperative IO monad (note: not all monads are imperative!) implicit in space allocation, and timing is state relative to the imperative real world. Using lazy even with optional eager coproducts leaks "laziness" into inner coproducts, because with lazy the laziness incorrectness originates from the outer function (see the example in the Non-termination section, where == is an outer binary operator function). This is because coproducts are bounded by finality, i.e. coinductive types with a final algebra on an final object[11].
Lazy causes indeterminism in the design and debugging of functions for latency and space, the debugging of which is probably beyond the capabilities of the majority of programmers, because of the dissonance between the declared function hierarchy and the runtime order-of-evaluation. Lazy pure functions evaluated with eager, could potentially introduce previously unseen non-termination at runtime. Conversely, eager pure functions evaluated with lazy, could potentially introduce previously unseen space and latency indeterminism at runtime.
Non-termination
在编译时,由于图灵完备语言中的停止问题和相互递归,函数通常不能保证终止。
急切的
用eager而不是lazy,对于Head”和“Tail”的连接,如果Head或Tail没有终止,则分别选择List(Head(), Tail())。tail == tail()或List(Head(), tail())。head == head()不为真,因为左边不终止,而右边终止。
而对于lazy,两边都终止。因此,对于连接产品,eager太过急切,在那些不需要它的情况下使用非终止符(包括运行时异常)。
懒惰的
对于lazy而不是eager,对于1”或“2”的析取,如果f不终止,则List(f ?1: 2, 3)。尾巴== (f ?List(1,3): List(2,3)。尾不为真,因为左边终止,右边不终止。
然而,由于渴望,任何一方都不终止,所以相等检验永远达不到。因此lazy懒得处理析取的副产品,在这些情况下,在做了比eager更多的工作后未能终止(包括运行时异常)。
[10]声明性延续和直言性二元性,Filinski,章节2.5.4 CBV和CBN的比较,以及3.6.1 SCL中的CBV和CBN。
[11]声明性延续和范畴对偶性,Filinski,第2.2.1节产物和副产物,2.2.2节终端和初始对象,2.5.2节CBV与惰性产物,2.5.3节CBN与急切副产物。
对于这些并没有明确客观的定义。以下是我对它们的定义:
命令式——重点是计算机应该采取什么步骤,而不是计算机将会做什么(例如C, c++, Java)。
声明性——重点是计算机应该做什么,而不是它应该如何做(例如SQL)。
函数式——声明性语言的子集,非常注重递归
简而言之:
命令式语言指定了计算机按顺序执行的一系列指令(做这个,然后做那个)。
声明性语言声明了一组规则,说明哪些输入应该产生哪些输出(例如:如果你有A,那么结果就是B).引擎会将这些规则应用于输入,并给出输出。
函数式语言声明了一组数学/逻辑函数,这些函数定义了如何将输入转换为输出。如。F (y) = y * y.它是一种声明性语言。