函数式编程、声明式编程和命令式编程是什么意思?


当前回答

在写这篇文章的时候,本页上投票最多的答案在声明式和命令式的定义上是不精确的,包括引用维基百科的答案。一些答案是用不同的方式合并了这两个术语。

还可以参考我对电子表格编程为什么是声明性的解释,而不管公式是否会改变单元格。

另外,有几个答案声称函数式编程必须是声明式编程的一个子集。在这一点上,这取决于我们是否区分“功能”和“过程”。让我们先处理命令式和声明式。

声明式表达式的定义

唯一可能区分声明式表达式和命令式表达式的属性是其子表达式的引用透明性(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与急切副产物。

其他回答

命令式——表达式描述要执行的动作序列(关联的)

声明性——表达式是对程序行为做出贡献的声明(关联、交换、幂等、单调)

函数式表达式的值仅为效果;语义支持等式推理

命令式和声明式描述了两种相反的编程风格。命令式是传统的“循序渐进”方法,而声明式则更多地是“这就是我想要的,现在你来研究如何去做”。

这两种方法贯穿于整个编程过程——即使使用相同的语言和相同的程序。一般来说,声明式方法被认为是更可取的,因为它使程序员不必指定如此多的细节,同时也减少了出现错误的机会(如果您描述了您想要的结果,并且一些经过良好测试的自动流程可以从该结果向后工作以定义步骤,那么您可能希望事情比手工指定每个步骤更可靠)。

另一方面,命令式方法为您提供了更多的低级控制——这是编程的“微观管理方法”。这可以让程序员利用有关问题的知识来给出更有效的答案。因此,程序的某些部分以声明式的风格编写并不罕见,但对速度至关重要的部分则更加必要。

as you might imagine, the language you use to write a program affects how declarative you can be - a language that has built-in "smarts" for working out what to do given a description of the result is going to allow a much more declarative approach than one where the programmer needs to first add that kind of intelligence with imperative code before being able to build a more declarative layer on top. so, for example, a language like prolog is considered very declarative because it has, built-in, a process that searches for answers.

so far, you'll notice that i haven't mentioned functional programming. that's because it's a term whose meaning isn't immediately related to the other two. at its most simple, functional programming means that you use functions. in particular, that you use a language that supports functions as "first class values" - that means that not only can you write functions, but you can write functions that write functions (that write functions that...), and pass functions to functions. in short - that functions are as flexible and common as things like strings and numbers.

it might seem odd, then, that functional, imperative and declarative are often mentioned together. the reason for this is a consequence of taking the idea of functional programming "to the extreme". a function, in it's purest sense, is something from maths - a kind of "black box" that takes some input and always gives the same output. and that kind of behaviour doesn't require storing changing variables. so if you design a programming language whose aim is to implement a very pure, mathematically influenced idea of functional programming, you end up rejecting, largely, the idea of values that can change (in a certain, limited, technical sense).

如果你这样做——如果你限制变量的改变方式——那么你几乎会意外地迫使程序员编写更具声明性的程序,因为命令式编程的很大一部分是描述变量如何改变,而你再也不能这样做了!因此,函数式编程——特别是用函数式语言编程——倾向于给出更多的声明性代码。

总结一下:

命令式和声明式是两种相反的编程风格(鼓励这些风格的编程语言使用相同的名称) 函数式编程是一种编程风格,其中函数变得非常重要,因此,更改值变得不那么重要。在值中指定更改的有限能力迫使使用更声明性的风格。

因此,“函数式编程”通常被描述为“声明式的”。

因为我写了我之前的答案,我制定了一个声明性属性的新定义,下面引用了它。我还将命令式编程定义为对偶属性。

这个定义比我在之前的回答中提供的定义更优越,因为它更简洁,更普遍。但它可能更难以理解,因为适用于编程和一般生活的不完备定理的含义对人类来说是很难理解的。

引用的定义解释讨论了纯函数式编程在声明式编程中所扮演的角色。

所有外来的编程类型都符合以下声明式和命令式的分类,因为下面的定义声称它们是二元的。

Declarative vs. Imperative The declarative property is weird, obtuse, and difficult to capture in a technically precise definition that remains general and not ambiguous, because it is a naive notion that we can declare the meaning (a.k.a semantics) of the program without incurring unintended side effects. There is an inherent tension between expression of meaning and avoidance of unintended effects, and this tension actually derives from the incompleteness theorems of programming and our universe. It is oversimplification, technically imprecise, and often ambiguous to define declarative as “what to do” and imperative as “how to do”. An ambiguous case is the “what” is the “how” in a program that outputs a program— a compiler. Evidently the unbounded recursion that makes a language Turing complete, is also analogously in the semantics— not only in the syntactical structure of evaluation (a.k.a. operational semantics). This is logically an example analogous to Gödel's theorem— “any complete system of axioms is also inconsistent”. Ponder the contradictory weirdness of that quote! It is also an example that demonstrates how the expression of semantics does not have a provable bound, thus we can't prove2 that a program (and analogously its semantics) halt a.k.a. the Halting theorem. The incompleteness theorems derive from the fundamental nature of our universe, which as stated in the Second Law of Thermodynamics is “the entropy (a.k.a. the # of independent possibilities) is trending to maximum forever”. The coding and design of a program is never finished— it's alive!— because it attempts to address a real world need, and the semantics of the real world are always changing and trending to more possibilities. Humans never stop discovering new things (including errors in programs ;-). To precisely and technically capture this aforementioned desired notion within this weird universe that has no edge (ponder that! there is no “outside” of our universe), requires a terse but deceptively-not-simple definition which will sound incorrect until it is explained deeply. Definition: The declarative property is where there can exist only one possible set of statements that can express each specific modular semantic. The imperative property3 is the dual, where semantics are inconsistent under composition and/or can be expressed with variations of sets of statements. This definition of declarative is distinctively local in semantic scope, meaning that it requires that a modular semantic maintain its consistent meaning regardless where and how it's instantiated and employed in global scope. Thus each declarative modular semantic should be intrinsically orthogonal to all possible others— and not an impossible (due to incompleteness theorems) global algorithm or model for witnessing consistency, which is also the point of “More Is Not Always Better” by Robert Harper, Professor of Computer Science at Carnegie Mellon University, one of the designers of Standard ML. Examples of these modular declarative semantics include category theory functors e.g. the Applicative, nominal typing, namespaces, named fields, and w.r.t. to operational level of semantics then pure functional programming. Thus well designed declarative languages can more clearly express meaning, albeit with some loss of generality in what can be expressed, yet a gain in what can be expressed with intrinsic consistency. An example of the aforementioned definition is the set of formulas in the cells of a spreadsheet program— which are not expected to give the same meaning when moved to different column and row cells, i.e. cell identifiers changed. The cell identifiers are part of and not superfluous to the intended meaning. So each spreadsheet result is unique w.r.t. to the cell identifiers in a set of formulas. The consistent modular semantic in this case is use of cell identifiers as the input and output of pure functions for cells formulas (see below). Hyper Text Markup Language a.k.a. HTML— the language for static web pages— is an example of a highly (but not perfectly3) declarative language that (at least before HTML 5) had no capability to express dynamic behavior. HTML is perhaps the easiest language to learn. For dynamic behavior, an imperative scripting language such as JavaScript was usually combined with HTML. HTML without JavaScript fits the declarative definition because each nominal type (i.e. the tags) maintains its consistent meaning under composition within the rules of the syntax. A competing definition for declarative is the commutative and idempotent properties of the semantic statements, i.e. that statements can be reordered and duplicated without changing the meaning. For example, statements assigning values to named fields can be reordered and duplicated without changed the meaning of the program, if those names are modular w.r.t. to any implied order. Names sometimes imply an order, e.g. cell identifiers include their column and row position— moving a total on spreadsheet changes its meaning. Otherwise, these properties implicitly require global consistency of semantics. It is generally impossible to design the semantics of statements so they remain consistent if randomly ordered or duplicated, because order and duplication are intrinsic to semantics. For example, the statements “Foo exists” (or construction) and “Foo does not exist” (and destruction). If one considers random inconsistency endemical of the intended semantics, then one accepts this definition as general enough for the declarative property. In essence this definition is vacuous as a generalized definition because it attempts to make consistency orthogonal to semantics, i.e. to defy the fact that the universe of semantics is dynamically unbounded and can't be captured in a global coherence paradigm. Requiring the commutative and idempotent properties for the (structural evaluation order of the) lower-level operational semantics converts operational semantics to a declarative localized modular semantic, e.g. pure functional programming (including recursion instead of imperative loops). Then the operational order of the implementation details do not impact (i.e. spread globally into) the consistency of the higher-level semantics. For example, the order of evaluation of (and theoretically also the duplication of) the spreadsheet formulas doesn't matter because the outputs are not copied to the inputs until after all outputs have been computed, i.e. analogous to pure functions. C, Java, C++, C#, PHP, and JavaScript aren't particularly declarative. Copute's syntax and Python's syntax are more declaratively coupled to intended results, i.e. consistent syntactical semantics that eliminate the extraneous so one can readily comprehend code after they've forgotten it. Copute and Haskell enforce determinism of the operational semantics and encourage “don't repeat yourself” (DRY), because they only allow the pure functional paradigm. 2 Even where we can prove the semantics of a program, e.g. with the language Coq, this is limited to the semantics that are expressed in the typing, and typing can never capture all of the semantics of a program— not even for languages that are not Turing complete, e.g. with HTML+CSS it is possible to express inconsistent combinations which thus have undefined semantics. 3 Many explanations incorrectly claim that only imperative programming has syntactically ordered statements. I clarified this confusion between imperative and functional programming. For example, the order of HTML statements does not reduce the consistency of their meaning.


编辑:我在Robert Harper的博客上发表了以下评论:

in functional programming ... the range of variation of a variable is a type Depending on how one distinguishes functional from imperative programming, your ‘assignable’ in an imperative program also may have a type placing a bound on its variability. The only non-muddled definition I currently appreciate for functional programming is a) functions as first-class objects and types, b) preference for recursion over loops, and/or c) pure functions— i.e. those functions which do not impact the desired semantics of the program when memoized (thus perfectly pure functional programming doesn't exist in a general purpose denotational semantics due to impacts of operational semantics, e.g. memory allocation). The idempotent property of a pure function means the function call on its variables can be substituted by its value, which is not generally the case for the arguments of an imperative procedure. Pure functions seem to be declarative w.r.t. to the uncomposed state transitions between the input and result types. But the composition of pure functions does not maintain any such consistency, because it is possible to model a side-effect (global state) imperative process in a pure functional programming language, e.g. Haskell's IOMonad and moreover it is entirely impossible to prevent doing such in any Turing complete pure functional programming language. As I wrote in 2012 which seems to the similar consensus of comments in your recent blog, that declarative programming is an attempt to capture the notion that the intended semantics are never opaque. Examples of opaque semantics are dependence on order, dependence on erasure of higher-level semantics at the operational semantics layer (e.g. casts are not conversions and reified generics limit higher-level semantics), and dependence on variable values which can not be checked (proved correct) by the programming language. Thus I have concluded that only non-Turing complete languages can be declarative. Thus one unambiguous and distinct attribute of a declarative language could be that its output can be proven to obey some enumerable set of generative rules. For example, for any specific HTML program (ignoring differences in the ways interpreters diverge) that is not scripted (i.e. is not Turing complete) then its output variability can be enumerable. Or more succinctly an HTML program is a pure function of its variability. Ditto a spreadsheet program is a pure function of its input variables. So it seems to me that declarative languages are the antithesis of unbounded recursion, i.e. per Gödel's second incompleteness theorem self-referential theorems can't be proven. Lesie Lamport wrote a fairytale about how Euclid might have worked around Gödel's incompleteness theorems applied to math proofs in the programming language context by to congruence between types and logic (Curry-Howard correspondence, etc).

我认为你的分类是不正确的。有两种相反的类型,命令式和声明式。函数式只是声明式的一个子类型。顺便说一句,维基百科也说了同样的事实。

现在,新的焦点是:我们需要旧的分类吗?

命令式/声明式/函数式在过去很好地分类了泛型语言,但是现在所有的“大语言”(如Java、Python、Javascript等)都有一些选项(通常是框架)来表达其主要关注点(通常是命令式)之外的“其他关注点”,并表达并行进程、声明式函数、lambdas等。

所以这个问题的一个很好的变体是“今天对框架进行分类的优点是什么?” ... 一个重要的方面是我们可以标记为“编程风格”……

重点研究数据与算法的融合

这是一个很好的例子。你可以在维基百科上读到jQuery,

jQuery的一系列核心特性——DOM元素选择、遍历和操作——由它的选择器引擎(…)支持,创建了一种新的“编程风格”,融合了算法和DOM数据结构

因此jQuery是关注“新编程风格”的最佳(流行)例子,它不仅是面向对象的,而且是“融合算法和数据结构”。jQuery有点像电子表格,但不是“面向单元格”,是“面向dom节点”…比较本文中的主要风格:

No fusion: in all "big languages", in any Functional/Declarative/Imperative expression, the usual is "no fusion" of data and algorithm, except by some object-orientation, that is a fusion in strict algebric structure point of view. Some fusion: all classic strategies of fusion, in nowadays have some framework using it as paradigm... dataflow, Event-driven programming (or old domain specific languages as awk and XSLT)... Like programming with modern spreadsheets, they are also examples of reactive programming style. Big fusion: is "the jQuery style"... jQuery is a domain specific language focusing on "fusing algorithms and DOM-data-structures". PS: other "query languages", as XQuery, SQL (with PL as imperative expression option) are also data-algorith-fusion examples, but they are islands, with no fusion with other system modules... Spring, when using find()-variants and Specification clauses, is another good fusion example.