函数式,声明式和命令式编程


466

功能性,声明性和命令式编程的含义是什么?


3
这里有一些很好的答案。未完全阐明一个有趣的事情是,声明必要的补充和共生,多只不同风格或什么如何
套件

1
@Kit Imo,此页面上的一些答案正在混淆这些条款。DP ==参考透明性(RT)。DP和IP是相反的,因此afaics不能作为一个整体的补充,也就是说,整个程序可以用任何一种风格编写。对函数的调用可以是DP(RT)或IP,其实现可以是混合或混合。从某种意义上说,它们不是共生的,否则调用DP功能中的IP功能可以使DP功能成为IP。它们是共生的,因为在现实世界中(例如功能性反应式)程序可以采用多种混合方式,例如IP顶级调用DP功能。
谢尔比·摩尔三世

应该添加到Wiki或类似于Wiki的链接,等等。这是Wikipedia en.wikipedia.org/wiki/Comparison_of_programming_paradigms


1
正在Meta上讨论此问题:meta.stackoverflow.com/q/342784/2751851
duplode '17

Answers:


262

在撰写本文时,此页面上投票最多的答案是不准确的,在声明式和命令式定义之间(包括引用维基百科的答案)是混乱的。一些答案以不同的方式混淆了这些术语。

另请参阅我对为何电子表格编程是声明性的解释,而无论公式如何使单元格发生突变。

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

声明式表达的定义

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

100%声明性语言(即,每个可能的表达式都使用RT的一种语言)(除其他RT要求之外)不允许对存储值(例如HTML和大多数Haskell)进行突变。

RT表达的定义

RT通常被称为“无副作用”。术语效果并没有一个确切的定义,所以有些人不同意,“无副作用”是一样的RT。RT有精确的定义

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

纯函数的定义

通常认为纯功能没有“副作用”。术语效果并没有一个确切的定义,所以有些人不同意。

纯函数具有以下属性。

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

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

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

RT的导数属性

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

注意,外部值的不变性是RT要求的一部分。

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

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

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

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

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

评估顺序

仅当任何函数调用不是RT(即,函数不是纯函数)(例如,在函数内访问函数外部的某些可变状态)时,子表达式的评估顺序的选择才能给出变化的结果。

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

而如果函数fgh不是纯函数,则选择评估顺序可以得出不同的结果。

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

切向,如果所有的识别符,例如abcd,是不可变的无处不在,状态外部程序不能被访问(即I / O),并且不存在抽象层破损,那么功能总是纯的。

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

评估订单详细信息

函数是从输入到输出的状态转换(不是可变的存储值)。对于函数调用的RT组合,这些状态转换的执行顺序是独立的。由于没有副作用以及RT函数可以用其缓存的值代替的原理,每个函数调用的状态转换都独立于其他函数调用。为了纠正一个普遍的误解,尽管Haskell的monad 可以说是不纯净的,所以绝对单反的构成始终声明性的,并且RT绝对是程序外部状态的必然要求(但是从下面的警告中可以看出,副作用被隔离)。IOWorld

急切的评估意味着在调用函数之前先评估函数的参数,而懒惰的评估意味着直到(以及是否)在函数中对其进行访问之前,才对参数进行评估

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

从概念上讲,所有表达式为常数而没有输入的函数,一元运算符是具有一个输入功能,二进制中缀运算符是具有两个输入功能,构造是函数,甚至控制语句(例如,(一个的组合物)的函数调用,例如ifforwhile)可以用函数建模。在为了使这些 参数的功能(带有嵌套函数调用顺序不要混淆)进行评估不是由语法申报,如f( g() )可以热切地评估g,然后fg的结果,也可能评估f,只有延迟计算g时内需要的结果f

需要说明的是,没有一种图灵完整的语言(即允许无限制的递归)是完全声明性的,例如,懒惰的评估会引入内存和时间不确定性。但是,由于选择评估顺序而产生的这些副作用仅限于内存消耗,执行时间,等待时间,非终止和外部滞后,从而导致外部同步。

功能编程

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

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

函数式编程通常使函数成为一类对象,这意味着函数类型可以在语法中出现在任何其他类型可能出现的任何地方。结果是功能可以输入功能并在功能上进行操作,从而通过强调功能组成(即,将确定性计算的子运算之间的依赖项分离)来提供关注点分离。

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

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

并行性

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

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

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

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

FP评估顺序

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

渴望(CBV)和懒惰(CBN)是绝对决斗[ 10 ],因为它们的评估顺序相反,即分别首先评估外部功能还是内部功能。想象一下倒置的树,然后急切地从功能树分支求值,将分支层次结构推向顶层功能主干;而懒惰则从主干到分支提示进行评估。渴望的人没有合积(“和”,a / k / a类别的“积”),懒惰的人没有合取的副产品(“或”,a / k / a类别的“和”)[ 11 ]。

性能

  • 急于

    与非终止一样,渴望过于渴望联合功能组成,即,组成控制结构完成了懒惰无法完成的不必要工作。对于例如,渴望急切和不必要的映射的整个列表,以布尔值,当它是由具有一个折叠,第一真实元件上终止。

    这项不必要的工作是要求将“ 纯粹”功能的“渴望”与“懒惰”的顺序时间复杂度“最多”提高到一个额外的log n因子的原因。一种解决方案是将函子(例如列表)与惰性构造函数一起使用(即,渴望使用可选的惰性产品),因为渴望的不正确性源自内部函数。这是因为乘积是构造性类型,即归纳类型,其初始代数在初始固定点上[ 11 ]

  • 与非终止一样,懒惰对于分离的功能组成也过于懒惰,也就是说,共性最终性可能会比必要的发生晚,从而导致不必要的工作和不确定性的延迟,而不是渴望[ 10 ] [ 11 ] 。最终状态的示例包括状态,时间,非终止和运行时异常。这些是命令性的副作用,但是即使使用纯声明性语言(例如Haskell),命令性IO monad中也存在状态隐含在空间分配中(注意:并非所有monads都是命令性的!),时序是相对于命令性而言的状态。真实世界。即使使用了可选的急切的副产品,也使用“惰性”会将“惰性”泄漏到内部副产品中,因为对于惰性,惰性的不正确性源自外部函数(请参见“非终止”部分中的示例,其中==是外部二进制运算符函数)。这是因为副产品受到最终性的限制,即在最终对象上具有最终代数的共归类型[ 11 ]。

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

不终止

在编译时,由于Halting问题和图灵完整语言中的相互递归,通常无法保证函数会终止。

  • 急于

    急切但又不懒惰,对于Head“ and” 的连词Tail,如果一个HeadTail一个不终止,则分别为一个List( Head(), Tail() ).tail == Tail()List( Head(), Tail() ).head == Head()不正确,因为左侧不终止,右侧也终止。

    而懒惰的双方都终止了。因此,渴望使用联合产品,并且在不需要时使用非终止(包括运行时异常)。

  • 懒惰但不急于,对于1“ or” 的析取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 SB中的CBV和CBN。

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


即使使用声明性约束编程,在求解器查找解决方案时约束也不会发生变化。这是显而易见的,因为无法指定更改时间。甚至在运行求解程序以找到解决方案之前,都会指定其他约束条件。这类似于电子表格中的声明
谢尔比·摩尔三世

3
缩写并不意味着提供定义。在我写“ RT通常缩写为'没有副作用'”的地方,这并不意味着RT的定义是“没有副作用”,因为人们对“效果”的定义可能有所不同。如果我改说“ RT通常缩写为'xyz'”,则无意义的符号不会为RT赋予任何定义。RT具有精确的定义,无论使用什么符号来引用它,它都不会改变。
谢尔比·摩尔三世

对于我声称每种DP都是RT的说法,我找不到反例。例如,上下文相关语法的术语的含义(即值)不会在语法中的其他时间或位置发生变异。请参阅上面的约束编程注释。
谢尔比·摩尔三世

1
在State monad中将RT与ESP样式等同于C无效的,因为每个C语句都可能会改变全局状态,而在state monad的“内部”,每个对应的语句都会生成该状态的COPY(如此修改)。后者是RT-前者不是。单子组成始终是RT。DP == RT是DP的唯一含义,它是一组不相交的属性(我是正确的数学证明,否则DP毫无意义)。
谢尔比·摩尔三世

1
我希望我能在第一句话之后理解这一点。我正在阅读DAX手册,该手册指出这是一种“功能语言”。这是什么意思?我不知道去问你的流行音乐。
Nick.McDermaid

103

这些实际上并没有任何明确,客观的定义。这是如何定义它们:

势在必行 -重点在于计算机应采取的步骤,而不是计算机将要执行的操作(例如C,C ++,Java)。

声明式 -重点在于计算机应该做什么而不是应该如何做(例如SQL)。

功能性 -声明性语言的子集,主要关注递归


1
请记住以下几点:1)解释的目的是简单而不是包罗万象2)就像我说的,定义这些语言有多种方法。因此,答案很可能对您是错误的,对其他人是正确的。
杰森·贝克

3
函数式编程不是 “声明性语言的子集”。声明式编程要求存储值具有不变性,如果不是 FP ,则函数式编程则不需要。看我的回答。另请参见电子表格单元格说明。正确的目标定义不是“模棱两可”的。命令式编程还着眼于“计算机应该做什么”。该唯一的区别是必要的程序必须处理可变的存储值。
谢尔比·摩尔三世

5
@ShelbyMooreIII-我倾向于同意Eric Meijer的观点。实际上并没有所谓的“非纯功能语言”。就我而言,Ocaml,F#等是具有功能性数据结构的命令性语言。但是,正如我在回答中所说,我不相信这个问题会有任何客观,明确的答案。有多种定义事物的方法。
杰森·贝克

3
当定义没有一个是明确的时,因为所选属性不是不相交的集合,因此可以数学上证明他正在混合术语。如果将FP 定义为 FP(即RT),则它与DP没有区别,请参见。我的回答。FP的不相交属性包括一等函数类型,它可以是命令函数。我在这里这里发现更多基本术语含糊不清。首选 FP与仅FP的定义正交。
谢尔比·摩尔三世

21
@ShelbyMooreIII-我假设OP希望用英语而不是Math Nerd-ese回答。如果那是一个无效的假设,那么我表示歉意。
杰森·贝克

54

命令式声明式描述了两种相反的编程风格。命令式是传统的“循序渐进配方”方法,而声明式则更是“这就是我想要的,现在您要弄清楚该怎么做”。

这两种方法在整个编程过程中都会发生-即使使用相同的语言和相同的程序。通常,声明性方法被认为是更可取的,因为它使程序员不必指定太多细节,同时也减少了发生错误的机会(如果您描述了所需的结果,并且经过良好测试的自动过程可以从此倒退为定义步骤,那么您可能希望事情比必须手动指定每个步骤更加可靠)。

另一方面,命令式方法为您提供了更底层的控制-这是编程的“微管理器方法”。这可以使程序员利用有关问题的知识来给出更有效的答案。因此,以声明性的方式编写程序的某些部分并不罕见,而对于速度至关重要的部分则更为必要。

就像您想象的那样,用于编写程序的语言会影响您的声明式能力-一种内置“智能”语言的语言,用于在给出结果描述的情况下确定要做什么,这将使声明性更强这种方法比程序员需要先在命令性代码中添加这种智能,然后才能够在顶部构建更具说明性的层的方法要多。因此,例如,像prolog这样的语言被认为是非常声明性的,因为它具有内置的搜索答案的过程。

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

因此,经常将功能性,命令性和声明性一起提到是很奇怪的。原因是将功能编程的想法“极端化”的结果。从最纯粹的意义上讲,一个函数是数学中的东西-一种“黑匣子”,需要一些输入并始终提供相同的输出。这种行为不需要存储更改的变量。因此,如果您设计一种旨在实现非常纯净,受数学影响的函数式编程思想的编程语言,您最终会在很大程度上拒绝可以改变的值(在某种特定的,有限的技术意义上)的思想。

如果这样做-如果限制了变量的更改方式-几乎偶然地,您最终会迫使程序员编写更具声明性的程序,因为命令式编程的很大一部分都在描述变量的更改方式,而您将无法再去做!因此,事实证明,函数式编程(尤其是使用函数式语言的编程)往往会提供更具说明性的代码。

总结一下,然后:

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

  • 函数式编程是一种编程风格,其中函数变得非常重要,结果,更改值变得不那么重要。指定值更改的能力有限,从而强制了更具声明性的样式。

因此“函数式编程”通常被称为“声明式”。


5
到目前为止最好的解释。看来Functional和OOP与命令式和声明式正交。
Didier A.

您会说逻辑编程是声明性的吗?还是它本身是正交的?
Didier A.

51

简而言之:

一个命令式语言 specfies一系列指令计算机执行序列(这样做,那么做)。

一个说明性语言宣布了一组关于应该是什么结果输出从输入(例如,如果你有,那么结果是B)的规则。引擎会将这些规则应用于输入,并给出输出。

功能语言声明了一组用于定义输入如何被转换到输出的数学/逻辑功能。例如。f(y)= y * y。它是一种声明性语言。


1
函数式编程不是 “一种声明性语言”。声明式编程需要存储值的不变性,而不函数编程则不需要。看我的回答。另请参见电子表格单元格说明。命令式逻辑(又名指令)顺序执行的唯一原因是由于存在可变的存储值,结果取决于评估顺序。使用您的词汇表,“指令”可以(并且“规则”不能)对可变值进行操作。
谢尔比·摩尔三世

23

当务之急:如何实现我们的目标

   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

您是在描述函数式编程与非FP,而不是声明式与命令式编程。函数式编程与命令式和声明式编程之间的极性正交。声明式编程需要存储值的不变性,而不函数编程则不需要。看我的回答
谢尔比·摩尔三世

22

命令式编程是指任何形式的编程,其中程序是根据描述计算机将如何执行操作的指令来构造的。

声明式编程是指任何形式的编程,其中您的程序是问题或解决方案的描述-但没有明确指出工作的方式

函数式编程是通过评估函数和函数的功能进行编程...(严格定义)函数式编程是指通过定义无副作用的数学函数进行编程,因此它是声明式编程的一种形式,但它并不是唯一的一种声明式编程

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

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


函数式编程不是 “声明式编程的一种形式”。声明式编程需要存储值的不变性,而不函数编程则不需要。看我的回答。另请参见电子表格单元格说明。未定义“描述工作方式”中的术语“工作”。命令式逻辑(也称为“指令”)按顺序执行的唯一原因是,由于存在可变的存储值,结果取决于评估顺序。
谢尔比·摩尔三世

2
请看我在谈论纯函数式编程。这些范式可能会交叉,我不想陷入比较混合语言的困境。从理论上讲,至少函数式编程是关于函数的,而不是描述计算机如何执行每个计算-因此,我认为它是声明性的。
达菲德里斯,

我编辑了答案,然后在“函数式编程”部分下,添加了一个场景,我们可以认为FP始终是纯函数,而不纯的FP实际上是“过程编程”。很抱歉没有在早些时候包括该解释。
谢尔比·摩尔三世

13

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

声明式 -表达式是有助于程序行为的声明(关联,可交换,幂等,单调)

功能性 -表达式具有唯一作用的价值;语义支持方程式推理


1
声明式表达式有助于程序的预期行为,命令式表达式有助于计划的或非预期的。如果这是故意的语义,则声明式不必是可交换的和幂等的。我喜欢您简洁的功能本质,因此我对此表示赞同。
谢尔比摩尔三世

10

自从我写了先前的答案以来,我为声明性属性制定了新的定义,下面对此进行了引用。我也将命令式编程定义为对偶属性。

此定义优于我在先前的回答中提供的定义,因为它简洁明了,而且更笼统。但是,这可能更难理解,因为不完整定理的含义适用于编程和一般的生活,这对于人类来说很难全神贯注。

引用的定义定义讨论了函数式编程在声明式编程中的作用。

所有奇特的编程类型都适用于以下声明式和命令式分类法,因为以下定义声称它们是对偶。

声明式与命令式

声明性属性很奇怪,很钝并且很难在技术上精确的定义中捕获,而该定义仍然是通用且不明确的,因为这是一个天真的想法,我们可以声明程序的含义(即语义)而不会引起意外的副作用。在意义表达和避免意外影响之间存在着内在的张力,而这种张力实际上源自编程和宇宙的不完全性定理

将声明式定义为做什么而将命令式定义为如何做通常过于简单,技术上不精确。一个模糊的情况是输出程序的程序(编译器)中的“ What ”是“ how ”。

显然,使图灵语言完整无限制递归在语义上也类似,不仅在评估的句法结构(即操作语义)中。从逻辑上讲,这是一个类似于哥德尔定理的例子-“ 任何完整的公理系统也都不一致 ”。思考那句话的矛盾怪异!这也是一个示例,说明了语义表达如何没有可证明的界限,因此我们不能证明2(和其语义类似)程序也就是停止定理而停止。

不完全性定理源于我们宇宙的基本本质,正如热力学第二定律所指出的那样,“ (也就是独立可能性的数量)正在永远趋向最大 ”。程序的编码和设计从未完成-它还活着!-因为它试图满足现实世界的需求,并且现实世界的语义总是在变化并且趋向于更多可能性。人类永远不会停止发现新事物(包括程序错误;-)。

为了在这个没有边缘的奇怪宇宙中准确而技术地捕获上述期望的概念(思考!我们的宇宙没有“外部”),需要一个简短但看似不是简单的定义,在解释之前,这听起来是不正确的深。

定义:


声明性属性是只能存在一组可能表达每种特定模块化语义的语句的地方。

命令属性3是对偶的,其中语义在组成上是不一致的和/或可以用语句集的变体来表达。


声明式的这种定义在语义范围上是局部的,这意味着它要求模块化语义必须保持其一致的含义,而不管它在全局范围内的位置和方式如何被实例化和采用。因此,每个声明性模块化语义都应与所有可能的其他语义固有地正交,而不是不可能的(由于不完全性定理)全局算法或模型,以证明一致性,这也是Robert Harper教授的“ 更多并非总是更好 ” 的要点。卡耐基梅隆大学计算机科学专业的教授,是标准ML的设计师之一。

这些模块化声明性语义的实例包括类理论函子例如所述Applicative,标称打字,命名空间,命名字段,和WRT到语义的操作水平,那么纯功能的编程。

因此,经过精心设计的声明性语言可以更清楚地表达含义,尽管可以表达的内容失去了一般性,但是可以通过固有的一致性获得表达。

前述定义的一个示例是电子表格程序的单元格中的一组公式-当移至不同的列和行单元格(即,单元格标识符已更改)时,它们预期不会具有相同的含义。单元标识符是预期含义的一部分,而不是多余的。因此,每个电子表格结果对于一组公式中的单元格标识符都是唯一的。在这种情况下,一致的模块化语义是使用单元格标识符作为单元格公式的函数的输入和输出(请参见下文)。

超文本标记语言(又称HTML)(用于静态网页的语言)是(至少在HTML 5之前)没有表达动态行为的高度(但不是完全3)声明性语言的示例。HTML也许是最容易学习的语言。对于动态行为,通常将命令性脚本语言(如JavaScript)与HTML结合使用。没有JavaScript的HTML符合声明性定义,因为每种名义类型(即标签)在语法规则内的组成下都保持其一致的含义。

声明性的竞争定义是语义语句的交换性幂等属性,即,可以在不更改含义的情况下对语句进行重新排序和复制。例如,如果将名称分配给命名字段的语句是模块化的,则可以对其进行重新排序和重复,而无需更改程序的含义。名称有时暗示着一个顺序,例如单元格标识符包括它们的列和行位置-在电子表格上移动总计将更改其含义。否则,这些属性隐式要求全局语义的一致性。设计语句的语义通常是不可能的,因此如果随机排序或重复,则它们将保持一致,因为顺序和重复是语义固有的。例如,语句“ Foo存在”(或构造)和“ Foo不存在”(和破坏)。如果人们认为预期语义的地方随机性不一致,那么对于声明性属性,人们应该接受这一定义。本质上,此定义作为通用定义是虚无的,因为它试图使一致性与语义正交,即无视语义世界是动态无界的,并且不能在全局一致性范例中捕获的事实。

需要低级操作语义(的结构评估顺序)的交换和幂等属性,将操作语义转换为声明性的局部模块化语义,例如函数式编程(包括递归而不是命令式循环)。这样,实现细节的操作顺序就不会影响(即,在全球范围内传播)高层语义的一致性。例如,电子表格公式的求值顺序(理论上也包括重复)并不重要,因为只有在计算完所有输出之后(即类似于纯函数),才将输出复制到输入中。

C,Java,C ++,C#,PHP和JavaScript并不是特别声明性的。Copute的语法和Python的语法在声明上与预期的结果相关联,即一致的语法语义消除了多余的内容,因此人们在忘记代码后可以很容易地理解它们。Copute和Haskell强制执行操作语义的确定性,并鼓励“ 不要重复自己 ”(DRY),因为它们只允许使用纯功能范式。


2即使我们可以使用语言Coq来证明程序的语义,但仅限于在打字中表达的语义,而打字永远无法捕获程序的所有语义,即使对于不是图灵完整的,例如使用HTML + CSS可以表达不一致的组合,从而具有不确定的语义。

3许多解释错误地声称仅命令式编程具有语法顺序的语句。我澄清了命令式和函数式编程之间的这种混淆。例如,HTML语句的顺序不会降低其含义的一致性。


编辑:我在罗伯特·哈珀的博客上发表了以下评论

在函数式编程中...变量的变化范围是一种类型

根据一个人如何将功能性与命令式编程区分开来,命令式程序中的“可分配值”也可能具有限制其可变性的类型。

我目前对函数式编程唯一了解的定义是a)作为一流对象和类型的函数,b)优先于循环的递归,和/或c)纯函数,即不影响所需语义的函数记住时的程序代码(由于操作语义的影响(例如内存分配,因此在通用的指称语义中不存在完全纯净的功能编程)。

纯函数的幂等属性意味着可以用其值替换对变量的函数调用,而命令式命令的参数通常不是这种情况。纯函数似乎是对输入和结果类型之间未组合状态转换的声明。

但是纯函数的组成不能保持任何这种一致性,因为可以用纯函数编程语言(例如Haskell的IOMonad)对副作用(全局状态)命令过程进行建模,此外,完全不可能阻止这种情况的发生。任何图灵完整的纯函数式编程语言。

正如我在2012年写的那样,似乎在您最近的博客中对评论的共识类似,声明式编程是一种尝试,旨在捕捉预期的语义从不透明的观念。不透明语义的示例有:依赖顺序,在操作语义层依赖于高层语义的擦除(例如,强制转换不是转换,而统一的泛型限制了高层语义),以及依赖于无法检查的变量值(已证明)正确)。

因此,我得出的结论是,只有非图灵完整语言才可以声明。

因此,说明性语言的一个明确而独特的属性可能是证明其输出可以服从一些可计数的生成规则集。例如,对于未编写脚本(即图灵不完整)的任何特定HTML程序(忽略解释器分歧的方式的差异),其输出可变性就可以枚举。或更简洁地说,HTML程序是其可变性的纯函数。同样,电子表格程序是其输入变量的纯函数。

因此在我看来,声明性语言是无界递归的对立面 ,即,根据哥德尔的第二不完全性定理,自指定理无法得到证明。

Lesie Lamport 撰写了一个童话,讲述了Euclid可能如何围绕类型和逻辑(Curry-Howard对应关系等)的一致性围绕编程语言上下文中应用于数学证明的Gödel不完全性定理进行工作。


罗伯特·哈珀(Robert Harper)似乎对大多数声明性定义无意义与我达成了一致,但我认为他没有见过我。他的确接近我的定义,在此他讨论了指称语义,但他没有理解我的定义。该模型(表示语义)是更高级别的
谢尔比·摩尔三世

7

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

声明式编程:告诉“机器”您想做什么,然后让计算机确定如何做。

命令式示例

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

注意:区别不是简洁,复杂或抽象之一。如前所述,不同的是怎么 VS 什么


2
好一个,但如果两者都提供至少一个例子,那就更好!
Pardeep Jain

4

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

势在必行/声明/功能方面还是不错的,在过去,以通用的语言进行分类,但现在,所有的“大语文”(使用Java,Python,JavaScript等)有一定的选择(通常是框架)来表达与“另一个焦点”而不是其主要功能(通常是命令性功能),并表示并行流程,声明性功能,lambda等。

因此,此问题的一个很好的变体是“今天对框架进行分类的哪个方面是好的?” ...一个重要方面是我们可以标记“编程风格”的东西

专注于数据与算法的融合

一个很好的例子来解释。您可以在Wikipedia上了解有关jQuery的信息

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

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

  1. 无融合:在所有“大的语言”,在任何功能性/声明/势在必行表达,通常是数据和算法的“无融合”,除由一些面向对象,这是一个融合严格代数结构的观点。

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

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


3

声明式编程是通过在输入和输出之间表达一些永恒的逻辑进行编程,例如,在伪代码中,以下示例是声明性的:

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

output = factorial(argvec[0])

我们在这里只定义了一个称为“阶乘”的关系,并将输出和输入之间的关系定义为那个关系。从这里可以明显看出,关于任何结构化语言,声明式编程都可以得到一定程度的扩展。声明式编程的中心思想是不可变数据,如果将其分配给变量,则只执行一次,然后再执行一次。其他更严格的定义意味着可能根本没有副作用,这些语言有时被称为“纯声明性”。

命令式样式的相同结果将是:

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

output = a

在此示例中,我们没有在输入和输出之间表达任何永恒的静态逻辑关系,我们手动更改了内存地址,直到其中一个保持所需的结果为止。显而易见的是,所有语言都允许某种程度的声明性语义,但并非全部都允许命令性语义,某些“纯”声明性语言完全允许副作用和变异。

人们常说,声明性语言指定了“必须做什么”,而不是“如何做到”,我认为这是一个用词不当的地方,声明性程序仍然指定了从输入到输出必须如何获取,但是换句话说,您指定的关系必须是可有效计算的(重要术语,如果您不知道,请查找)。另一种方法是非确定性编程,它实际上只是指定结果要满足的条件,然后您的实现才花光所有尝试和错误的路径,直到成功为止。

纯粹的声明性语言包括Haskell和Pure Prolog。一个和另一个之间的滑动比例是:Pure Prolog,Haskell,OCaml,Scheme / Lisp,Python,Javascript,C-,Perl,PHP,C ++,Pascall,C,Fortran,Assembly


您没有定义函数式编程。您错误地暗示“某些'纯净的'声明性语言'”,声明性编程可能是不纯正的。声明式编程需要存储值的不变性,而命令式编程则不需要。看我的回答。不变性是“永恒”的品质-看到您的声明factorial不会改变任何值。
谢尔比·摩尔三世

3

关于标记的“类型”,这里有一些很好的答案。

我提出了一些通常与函数式编程人群相关的其他“异国情调”概念:

  • 特定领域语言DSL编程:创建一种新语言来解决当前的问题。
  • 元编程:当您的程序编写其他程序时。
  • 渐进式编程:您可以在其中构建一个不断改进自身或连续生成更好子程序的系统。

3

我认为您的分类法不正确。命令式和声明式有两种相反的类型。功能只是声明性的子类型。顺便说一句,维基百科指出了同样的事实。


+1:是的,范例是苹果和橘子。
Nikhil Chelliah,2009年

FP 并非 “仅仅是声明性的子类型”。FP与命令性vs. DP的极性正交。DP 需要存储值的不变性,而不 FP则不需要。Wikipedia将 FP与FP 混为一谈,荒谬的说法是以下概念“对命令式编程通常是陌生的”:一流的函数,递归,求值顺序和静态类型。然后维基百科承认不纯的 “非功能语言的功能编程”。
谢尔比·摩尔三世

维基百科在这一点上是错误的。如果您选择的话,许多流行的功能语言都允许以“声明性样式”进行编程,但不是声明性语言。但是对于C也是如此,如果您选择使用C,您仍然可以使用void * s以​​功能样式进行编程。
Plynx 2012年

也许我应该在这一点上更加清楚,但是从另一方面讲,我不会用不太相关的(imo)细节来搞乱主题入门者。我看到功能语言倾向于以声明方式使用。您可以尝试使用ASM或C进行声明式和/或功能性编写,也可以使用Lisp编写命令式程序,但我怀疑这对问题作者是否很有帮助或提供信息。因此,从本质上讲,即使措辞可能有所不同,我仍然认为我的答案是适当的。
罗里克2012年

2

简而言之,编程风格越强调“做什么”来抽象“怎样做”的细节,则该样式就越具有说明性。势在必行。函数式编程与声明式样式相关联。


请参阅其他答案下方的评论。FP并不总是声明性的。IP与DP的错误分类法是什么与如何分类,因为DP和IP都具有涉及什么和方法的逻辑。
谢尔比·摩尔三世
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.