476

What do the terms functional, declarative, and imperative programming mean?

4

14 回答 14

267

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

另请参阅我对电子表格编程为何是声明性的解释,而不管公式是否会改变单元格。

此外,一些答案声称函数式编程必须是声明式的子集。在这一点上,这取决于我们是否将“功能”与“程序”区分开来。让我们先处理命令​​式与声明式。

声明性表达的定义

唯一可以将声明式表达式与命令式表达式区分开来的属性是其子表达式的引用透明度 (RT)。所有其他属性要么在两种类型的表达式之间共享,要么从 RT 派生。

100% 的声明性语言(即所有可能的表达式都是 RT 的语言)不允许(以及其他 RT 要求)存储值的突变,例如 HTML 和大多数 Haskell。

RT 表达式的定义

RT 通常被称为“没有副作用”。效果一词没有精确的定义,因此有些人不同意“无副作用”与 RT 相同。RT 有一个精确的定义

如果对于所有程序,表达式e是引用透明的,如果p每次出现的einp都可以替换为评估的结果e,而不影响 的可观察结果p

由于每个子表达式在概念上都是一个函数调用,RT 要求函数的实现(即被调用函数内部的表达式)不能访问函数外部的可变状态(访问可变局部状态是允许)。简而言之,函数(实现)应该是pure

纯函数的定义

纯函数通常被称为“没有副作用”。效果这个词没有一个准确的定义,所以有些人不同意。

纯函数具有以下属性。

  • 唯一可观察的输出是返回值。
  • 唯一的输出依赖是参数。
  • 在生成任何输出之前,参数已完全确定。

请记住,RT 适用于表达式(包括函数调用),而纯度适用于(实现)函数。

产生 RT 表达式的不纯函数的一个不起眼的例子是并发,但这是因为纯度在中断抽象层被破坏了。你真的不需要知道这一点。要制作 RT 表达式,请调用纯函数。

RT的导数属性

声明式编程引用的任何其他属性,例如Wikipedia 使用的1999 年的引用,要么源自 RT,要么与命令式编程共享。从而证明我的精确定义是正确的。

请注意,外部值的不变性是 RT 要求的一个子集。

  • 声明性语言没有循环控制结构,例如forand while,因为由于不可变性,循环条件永远不会改变。

  • 声明性语言除了嵌套函数顺序(也称为逻辑依赖项)之外不表达控制流,因为 由于不可变性,评估顺序的其他选择不会改变结果(见下文)。

  • 声明式语言表达逻辑“步骤”(即嵌套的 RT 函数调用顺序),但每个函数调用是否是更高级别的语义(即“做什么”)并不是声明式编程的要求。与命令式的区别在于,由于不可变性(即更普遍的 RT),这些“步骤”不能依赖于可变状态,而仅依赖于所表达逻辑的关系顺序(即函数调用的嵌套顺序,也就是子表达式)。

例如,在对段落<p>中的子表达式(即标签)进行评估之前,无法显示 HTML 段落。由于标签层次的逻辑关系(子表达式的嵌套,类似嵌套的函数调用),没有可变状态,只有顺序依赖。

  • 因此存在不变性(更一般地 RT)的派生属性,即声明性表达式表达组成部分的逻辑关系(即子表达式函数参数的)而不是可变状态关系。

评估顺序

子表达式求值顺序的选择只能在任何函数调用不是 RT(即函数不是纯函数)时给出不同的结果,例如在函数内部访问函数外部的一些可变状态。

例如,给定一些嵌套表达式,例如,如果函数、和是纯函数,f( g(a, b), h(c, d) )则函数参数的急切和惰性求值将给出相同的结果。fgh

然而,如果函数fgh不是纯函数,则评估顺序的选择会给出不同的结果。

请注意,嵌套表达式在概念上是嵌套函数,因为表达式运算符只是伪装成一元前缀、一元后缀或二元中缀表示法的函数调用。

切线地,如果所有标识符,例如a, b, c, d,在任何地方都是不可变的,则程序外部的状态不能被访问(即 I/O),并且没有抽象层中断,那么函数总是纯函数。

顺便说一句,Haskell 有不同的语法,f (g a b) (h c d).

评估订单详情

函数是从输入到输出的状态转换(不是可变的存储值)。对于函数调用的 RT 组合,这些状态转换的执行顺序是独立的。由于没有副作用以及RT 函数可以被其缓存值替换的原理,每个函数调用的状态转换都独立于其他函数调用。为了纠正一个流行的误解,纯单子组合总是声明性和 RT,尽管 Haskell 的IO单子可以说是不纯的,因此World对于程序外部的状态是必要的(但在下面的警告的意义上,副作用被隔离)。

急切求值意味着函数参数在调用函数之前被求值,而惰性求值意味着直到(并且如果)它们在函数中被访问时才求值。

定义:函数参数在函数定义站点声明,函数参数在函数调用站点提供。了解参数参数之间的区别。

从概念上讲,所有表达式都是函数调用的(一个组合),例如,常量是没有输入的函数,一元运算符是具有一个输入的函数,二元中缀运算符是具有两个输入的函数,构造函数是函数,甚至是控制语句(例如if, for, while)可以用函数建模。评估这些 参数函数的顺序(不要与嵌套函数调用顺序混淆)不是由语法声明的,例如f( g() )可以急切地评估gthenfg结果,或者它可以评估f并且只有g在 需要它的结果时才懒惰地评估f

警告,没有图灵完备的语言(即允许无限递归)是完全声明性的,例如惰性求值引入了内存和时间不确定性。但是由于评估顺序的选择,这些副作用仅限于内存消耗、执行时间、延迟、不终止和外部滞后,因此是外部同步。

函数式编程

因为声明式编程不能有循环,所以迭代的唯一方法是函数递归。从这个意义上说,函数式编程与声明式编程有关。

函数式编程并不局限于声明式编程。功能组合可以与子类型形成对比,特别是在表达式问题方面,可以通过添加子类型或功能分解来实现扩展。扩展可以是两种方法的混合。

函数式编程通常使函数成为一等对象,这意味着函数类型可以出现在语法中任何其他类型可能出现的任何位置。结果是函数可以输入和操作函数,从而通过强调函数组合来提供关注点分离,即分离确定性计算的子计算之间的依赖关系。

例如,不是为可以应用于集合的每个元素的无限数量的可能的专门操作中的每一个编写单独的函数(如果函数还必须是声明性的,则使用递归而不是循环),函数式编程采用可重用迭代函数,例如map, fold, filter. 这些迭代函数输入一个一流的专用动作函数。这些迭代函数迭代集合并为每个元素调用输入专用操作函数。这些动作函数更简洁,因为它们不再需要包含循环语句来迭代集合。

但是,请注意,如果一个函数不是纯函数,那么它实际上是一个过程。我们或许可以争辩说,使用不纯函数的函数式编程实际上是过程式编程。因此,如果我们同意声明式表达式是 RT,那么我们可以说过程式编程不是声明式编程,因此我们可能会争辩说函数式编程始终是 RT,并且必须是声明式编程的子集。

并行性

这种具有一流功能的功能组合可以通过分离独立功能来表达并行性的深度。

布伦特原则:工作 w 和深度 d 的计算可以在时间 O(max(w/p, d)) 中在 p 处理器 PRAM 中实现。

并发性和并行性都需要声明式编程,即不变性和 RT。

那么并行==并发这个危险的假设是从哪里来的呢?这是具有副作用的语言的自然结果:当您的语言到处都有副作用时,任何时候您尝试一次做不止一件事时,您基本上都会因每个操作的效果交错而导致不确定性. 所以在副作用语言中,获得并行性的唯一方法是并发。因此,我们经常看到两者混为一谈也就不足为奇了。

FP评估顺序

请注意,评估顺序也会影响功能组合的终止和性能副作用。

Eager (CBV) 和lazy (CBN) 是分类决斗[ 10 ],因为它们具有颠倒的评估顺序,即分别先评估外部函数还是内部函数。想象一棵倒置的树,然后从功能树分支提示向上到分支层次结构到顶层功能主干进行渴望评估;然而,lazy 从树干到分支尖端进行评估。Eager 没有合取积(“and”,a/k/a 分类“products”),lazy 没有析取副积(“or”,a/k/a 分类“sums”)[ 11 ]。

表现

  • 渴望的

与非终止一样,eager 对合取函数组合过于急切,即组合控制结构做了惰性没有完成的不必要的工作。例如,当它由终止于第一个真正元素的折叠组成时,急切且不必要地将整个列表映射为布尔值。

这种不必要的工作是在急切与懒惰的顺序时间复杂度中声称的“最多”一个额外的log n 因素的原因,两者都具有纯函数。一个解决方案是使用带有惰性构造函数的函子(例如列表)(即使用可选的惰性产品进行急切),因为急切不正确源自内部函数。这是因为产品是构造类型,即在初始固定点上具有初始代数的归纳类型[ 11 ]

  • 懒惰的

与非终止一样,懒惰对于析取函数组合来说太懒了,即共归纳终结性可能比必要的晚发生,从而导致不必要的工作和迟到的不确定性,而 eager[ 10 ][ 11 ]不是这种情况. 最终性的示例是状态、计时、非终止和运行时异常。这些是命令式的副作用,但即使在纯声明式语言(例如 Haskell)中,命令式 IO monad 中也存在状态(注意:并非所有 monad 都是命令式的!)隐含在空间分配中,并且时间是相对于命令式的状态真实世界。即使与可选的渴望副产品一起使用惰性也会将“惰性”泄漏到内部副产品中,因为使用惰性时,惰性不正确性源于外部函数(请参阅非终止部分中的示例,其中 == 是外部二元运算符函数)。这是因为联积受终结性的限制,即在最终对象上具有最终代数的协约类型[ 11 ]。

Lazy 会导致函数设计和调试时出现延迟和空间的不确定性,由于声明的函数层次结构和运行时求值顺序之间的不协调,其调试可能超出了大多数程序员的能力。使用 eager 评估的惰性纯函数可能会在运行时引入以前看不见的非终止。相反,使用惰性求值的渴望纯函数可能会在运行时引入以前看不见的空间和延迟不确定性。

不终止

在编译时,由于图灵完备语言中的停机问题和相互递归,通常不能保证函数终止。

  • 渴望的

用急切但不懒惰,对于Head"and"的连词Tail,如果要么HeadTail不终止,则分别为要么List( Head(), Tail() ).tail == Tail()List( Head(), Tail() ).head == Head()不为真,因为左侧不终止,而右侧不终止。

然而,懒惰的双方都终止了。因此,eager 对合取积过于急切,并且在不需要的情况下不终止(包括运行时异常)。

  • 懒惰的

懒惰但不急切,对于1“或”的析取2,如果f不终止,List( f ? 1 : 2, 3 ).tail == (f ? List( 1, 3 ) : List( 2, 3 )).tail则不正确,因为左侧终止,而右侧不终止。

然而,由于渴望双方都不会终止,所以永远不会达到平等测试。因此,lazy 对析取副产品太懒了,并且在这些情况下,在完成比渴望更多的工作后无法终止(包括运行时异常)。

[ 10 ] 声明性连续性和分类对偶性,Filinski,第 2.5.4 节 CBV 和 CBN 的比较,以及 SCL 中的 3.6.1 CBV 和 CBN。

[ 11 ] 声明性连续性和分类对偶性,Filinski,第 2.2.1 节产品和副产品,2.2.2 终端和初始对象,2.5.2 带有惰性产品的 CBV,以及 2.5.3 带有热切副产品的 CBN。

于 2011-12-02T14:11:45.293 回答
104

There's not really any non-ambiguous, objective definition for these. Here is how I would define them:

Imperative - The focus is on what steps the computer should take rather than what the computer will do (ex. C, C++, Java).

Declarative - The focus is on what the computer should do rather than how it should do it (ex. SQL).

Functional - a subset of declarative languages that has heavy focus on recursion

于 2009-03-02T14:11:23.703 回答
59

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

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

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

正如您可能想象的那样,您用于编写程序的语言会影响您的声明性 - 一种具有内置“智能”功能的语言,可以根据结果描述确定要做什么,这将允许更具声明性这种方法比程序员需要先用命令式代码添加这种智能,然后才能在上面构建更具声明性的层。因此,例如,像 prolog 这样的语言被认为是非常具有声明性的,因为它内置了一个搜索答案的过程。

到目前为止,您会注意到我没有提到函数式编程。那是因为它是一个术语,其含义与其他两个没有直接关系。在最简单的情况下,函数式编程意味着您使用函数。特别是,您使用支持函数作为“第一类值”的语言 - 这意味着您不仅可以编写函数,而且可以编写编写函数的函数(编写函数......),并将函数传递给功能。简而言之 - 函数与字符串和数字一样灵活和通用。

那么,函数式、命令式和声明式经常被一起提到,这似乎很奇怪。其原因是将函数式编程的想法“发挥到极致”的结果。从最纯粹的意义上说,函数是数学中的东西——一种“黑匣子”,它接受一些输入并总是给出相同的输出。并且这种行为不需要存储变化的变量。因此,如果您设计一种编程语言,其目的是实现一个非常纯粹的、受数学影响的函数式编程理念,那么您最终会在很大程度上拒绝可以改变的值的理念(在一定的、有限的、技术意义上)。

如果你这样做了——如果你限制了变量的变化方式——那么你几乎会意外地迫使程序员编写更具声明性的程序,因为大部分命令式编程都在描述变量是如何变化的,而你不能再去做!所以事实证明,函数式编程——尤其是用函数式语言编程——往往会给出更多的声明性代码。

总结一下,那么:

  • 命令式和声明式是两种相反的编程风格(相同的名称用于鼓励这些风格的编程语言)

  • 函数式编程是一种编程风格,其中函数变得非常重要,因此改变值变得不那么重要。指定值更改的有限能力迫使采用更具声明性的样式。

所以“函数式编程”通常被描述为“声明式”。

于 2011-09-05T02:36:05.303 回答
50

简而言之:

命令式语言指定计算机按顺序执行的一系列指令(执行此操作,然后执行此操作)。

声明性语言声明了一组关于哪些输出应该由哪些输入产生的规则(例如,如果你有 A,那么结果就是 B)。引擎会将这些规则应用于输入,并给出输出。

函数式语言声明了一组数学/逻辑函数,这些函数定义了如何将输入转换为输出。例如。f(y) = y * y。它是一种声明性语言。

于 2009-03-02T17:18:51.740 回答
25

势在必行:如何实现我们的目标

   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
于 2011-10-23T18:45:57.033 回答
23

命令式编程是指任何编程风格,其中您的程序由描述计算机执行的操作将如何发生的指令构成。

声明式编程是指任何编程风格,其中您的程序是对问题或解决方案的描述——但没有明确说明工作将如何完成

函数式编程是通过评估函数和函数的函数来编程......因为(严格定义的)函数式编程意味着通过定义无副作用的数学函数进行编程,所以它是一种声明式编程,但它不是唯一一种声明式编程.

逻辑编程(例如在 Prolog 中)是另一种形式的声明式编程。它涉及通过确定逻辑语句是否为真(或是否可以满足)来进行计算。程序通常是一系列事实和规则——即描述而不是一系列指令。

术语重写(例如 CASL)是另一种形式的声明式编程。它涉及代数项的符号变换。它与逻辑编程和函数式编程完全不同。

于 2011-05-03T11:07:19.107 回答
16

imperative - expressions describe sequence of actions to perform (associative)

declarative - expressions are declarations that contribute to behavior of program (associative, commutative, idempotent, monotonic)

functional - expressions have value as only effect; semantics support equational reasoning

于 2012-08-07T01:27:38.480 回答
14
于 2013-03-13T10:06:30.587 回答
8

命令式编程:告诉“机器”如何做某事,结果你想要发生的事情就会发生。

声明式编程:告诉“机器”你想发生什么,让计算机弄清楚如何去做。

命令式示例

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);
}

注意:区别不在于简洁、复杂或抽象。如前所述,区别在于howwhat

于 2013-12-19T18:21:22.487 回答
4

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

于 2009-03-02T15:00:44.643 回答
4

如今,新的焦点:我们需要旧的分类吗?

过去, 命令式/声明式/功能性方面很好地对通用语言进行分类,但现在所有“大语言”(如 Java、Python、Javascript 等)都有一些选项(通常是框架)来表达“其他焦点”比其主要的(通常的命令式),并表达并行过程,声明性函数,lambdas等。

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

专注于数据与算法的融合

一个很好的例子来解释。正如您可以在 Wikipedia 上阅读有关 jQuery 的内容

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

因此,jQuery 是专注于“新编程风格”的最佳(流行)示例,这不仅是面向对象,而且是“融合 算法和数据结构”。jQuery 在电子表格中有些反应,但不是“面向单元格”,是“面向 DOM 节点”......比较此上下文中的主要样式:

  1. 不融合:在所有“大语言”中,在任何函数式/声明式/命令式表达式中,通常是数据和算法的“不融合”,除了某些面向对象,即严格代数结构观点的融合

  2. 一些融合:所有经典的融合策略,现在都有一些框架使用它作为范例...... 数据流事件驱动编程(或旧的领域特定语言,如awkXSLT)......就像使用现代电子表格进行编程一样,它们也是反应式编程风格的例子。

  3. 大融合:是“jQuery 风格”... jQuery 是一种专注于“融合算法和DOM 数据结构”的领域特定语言。
    PS:其他“查询语言”,如 XQuery、SQL(使用 PL 作为命令式表达式选项)也是数据算法融合示例,但它们是孤岛,与其他系统模块没有融合... Spring,当使用 find()-variants和规范条款,是另一个很好的融合例子。

于 2013-07-01T15:07:27.230 回答
3

Declarative programming is programming by expressing some timeless logic between the input and the output, for instance, in pseudocode, the following example would be declarative:

def factorial(n):
  if n < 2:
    return 1
  else:
    return factorial(n-1)

output = factorial(argvec[0])

We just define a relationship called the 'factorial' here, and defined the relationship between the output and the input as the that relationship. As should be evident here, about any structured language allows declarative programming to some extend. A central idea of declarative programming is immutable data, if you assign to a variable, you only do so once, and then never again. Other, stricter definitions entail that there may be no side-effects at all, these languages are some times called 'purely declarative'.

The same result in an imperative style would be:

a = 1
b = argvec[0]
while(b < 2):
  a * b--

output = a

In this example, we expressed no timeless static logical relationship between the input and the output, we changed memory addresses manually until one of them held the desired result. It should be evident that all languages allow declarative semantics to some extend, but not all allow imperative, some 'purely' declarative languages permit side effects and mutation altogether.

Declarative languages are often said to specify 'what must be done', as opposed to 'how to do it', I think that is a misnomer, declarative programs still specify how one must get from input to output, but in another way, the relationship you specify must be effectively computable (important term, look it up if you don't know it). Another approach is nondeterministic programming, that really just specifies what conditions a result much meet, before your implementation just goes to exhaust all paths on trial and error until it succeeds.

Purely declarative languages include Haskell and Pure Prolog. A sliding scale from one and to the other would be: Pure Prolog, Haskell, OCaml, Scheme/Lisp, Python, Javascript, C--, Perl, PHP, C++, Pascall, C, Fortran, Assembly

于 2010-06-08T14:21:54.427 回答
3

这里有一些关于提到的“类型”的好答案。

我提交了一些额外的、更“异国情调”的概念,这些概念通常与函数式编程人群相关:

  • 领域特定语言DSL编程:创建一种新语言来处理手头的问题。
  • 元编程:当您的程序编写其他程序时。
  • 进化编程:您构建一个不断改进自身或生成连续更好的子程序代的系统。
于 2013-04-09T21:13:13.737 回答
2

简而言之,一种编程风格越强调什么(做什么)抽象出如何(做什么)的细节,这种风格就越被认为是声明性的。命令式的情况正好相反。函数式编程与声明式风格相关联。

于 2009-08-20T21:29:53.387 回答