功能,声明和命令式编程这两个术语是什么意思?
在撰写本文时,此页面上的最高投票答案在声明性与命令性定义上是不精确和混淆的,包括引用维基百科的答案.一些答案以不同的方式混淆了这些术语.
另请参阅我对电子表格编程为什么是声明性的解释,无论公式如何改变单元格.
此外,一些答案声称函数式编程必须是声明性的子集.在这一点上,它取决于我们将"功能"与"程序"区分开来.让我们首先处理命令式与声明性.
声明性表达的定义
可能将声明性表达式与命令式表达式区分开来的唯一属性是其子表达式的引用透明度(RT).所有其他属性在两种类型的表达式之间共享,或者从RT派生.
100%声明性语言(即其中每个可能的表达式都是RT)不会(在其他RT要求中)允许存储值的变异,例如HTML和大多数Haskell.
RT表达式的定义
RT通常被称为"无副作用".术语效果没有精确的定义,因此有些人不同意"没有副作用"与RT相同.RT有一个精确的定义.
由于每个子表达式在概念上都是函数调用,因此RT要求函数的实现(即被调用函数内的表达式)可能无法访问函数外部的可变状态(访问可变局部状态是允许).简而言之,功能(实现)应该是纯粹的.
纯函数的定义
纯函数通常被认为具有"无副作用".术语效果并没有一个确切的定义,所以有些人不同意.
纯函数具有以下属性.
唯一可观察的输出是返回值.
唯一的输出依赖是参数.
在生成任何输出之前完全确定参数.
请记住,RT适用于表达式(包括函数调用),纯度适用于(函数的实现).
使用RT表达式的不纯函数的一个模糊示例是并发的,但这是因为在中断抽象层中纯度被破坏了.你真的不需要知道这一点.要制作RT表达式,可以调用纯函数.
RT的衍生属性
声明性编程引用的任何其他属性,例如维基百科使用的1999年引用,或者源自RT,或者与命令式编程共享.从而证明我的精确定义是正确的.
注意,外部值的不变性是RT要求的一个子集.
声明语言没有循环控制结构,例如for
和while
,因为由于不变性,循环条件永远不会改变.
声明性语言不表示嵌套函数顺序(又称逻辑依赖性)以外的控制流,因为 由于不变性,评估顺序的其他选择不会改变结果(见下文).
声明性语言表示逻辑"步骤"(即嵌套的RT函数调用顺序),但是每个函数调用是否是更高级别的语义(即"要做什么")不是声明性编程的要求.与命令的区别在于,由于不变性(即更一般地说是RT),这些"步骤"不能依赖于可变状态,而只依赖于表达逻辑的关系顺序(即函数调用的嵌套顺序,即子表达式) ).
例如,在评估段落中的子表达式(即标记)之前,不能显示HTML段落.由于标签层次结构的逻辑关系(子表达式的嵌套,类似嵌套的函数调用),因此没有可变状态,只有顺序依赖性.
因此,存在不变性的衍生属性(更一般地说是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)
.
评估订单详情
函数是从输入到输出的状态转换(不是可变的存储值).对于调用纯函数的RT组合,这些状态转换的执行顺序是独立的.由于缺乏副作用以及RT函数可能被其缓存值替换的原则,每个函数调用的状态转换独立于其他函数调用.为了纠正一个流行的误解,纯粹的monadic组合总是声明和RT,尽管事实上Haskell的IO
monad 可以说是不纯的,因此World
在程序外部的状态是必要的(但在下面的警告意义上,副作用是孤立的).
急切评估意味着在调用函数之前评估函数参数,而惰性评估意味着在函数内访问它们之前(以及如果)它们是否被评估.
定义:函数参数在函数定义站点声明,函数参数在函数调用站点提供.知道参数和参数之间的区别.
从概念上讲,所有表达式为常数而没有输入的函数,一元运算符是具有一个输入功能,二进制中缀运算符是具有两个输入端的功能,构造函数的功能,甚至控制语句(例如,(一个的组合物)的函数调用,例如if
,for
,while
)可以用函数建模.在这些命令 的参数功能(不使用嵌套函数调用为了混淆)进行评估不是由语法申报,如f( g() )
可以热切地评估g
,然后f
对g
的结果,也可能评估f
,只懒洋洋地评估g
时内需要的结果f
.
警告,没有图灵完整语言(即允许无限递归)是完美的声明,例如懒惰评估引入记忆和时间不确定性.但是由于评估顺序的选择而产生的这些副作用仅限于存储器消耗,执行时间,等待时间,非终止和外部滞后因此外部同步.
功能编程
因为声明性编程不能有循环,所以迭代的唯一方法是函数递归.从这个意义上讲,函数式编程与声明式编程有关.
但函数式编程不仅限于声明式编程.功能组合可以与子类型形成对比,特别是对于表达问题,其中可以通过添加子类型或功能分解来实现扩展.扩展可以是两种方法的混合.
函数式编程通常使函数成为第一类对象,这意味着函数类型可以出现在任何其他类型的语法中.结果是函数可以输入函数并对函数进行操作,从而通过强调函数组合来提供关注点分离,即分离确定性计算的子计算之间的依赖性.
例如,对于可以应用于集合的每个元素的无限多个可能的专用操作中的每一个,不是编写单独的函数(并且如果函数也必须是声明的,则使用递归而不是循环),函数编程使用可重用的迭代.功能,例如map
,fold
,filter
.这些迭代函数输入一流的专用动作函数.这些迭代函数迭代集合并为每个元素调用输入专用操作函数.这些操作函数更简洁,因为它们不再需要包含循环语句来迭代集合.
但是,请注意,如果函数不纯,那么它实际上是一个过程.我们或许可以争辩说使用不纯函数的函数式编程实际上是程序式编程.因此,如果我们同意声明性表达式是RT,那么我们可以说过程式编程不是声明式编程,因此我们可能会认为函数式编程总是RT并且必须是声明式编程的子集.
排比
这种具有一等函数的功能组合可以通过分离出独立的功能来表达并行性的深度.
布伦特原理:工作w和深度d的计算可以在时间O(max(w/p,d))中在p处理器PRAM中实现.
并发和并行性也需要声明性编程,即不变性和RT.
那么这个危险的假设是什么,Parallelism == Concurrency来自哪里呢?这是带有副作用的语言的自然结果:当你的语言到处都有副作用时,那么每次你尝试做多件事时你基本上都会产生由每个操作的效果交错引起的非确定性.因此,在副作用语言中,获得并行性的唯一方法是并发; 因此,我们经常看到两者混为一谈并不奇怪.
请注意,评估顺序还会影响功能组合的终止和性能副作用.
渴望(CBV)和懒惰(CBN)是分类决斗[ 10 ],因为它们具有相反的评估顺序,即分别是首先评估外部函数还是内部函数.想象一个倒置的树,然后急切地从功能树分支提示分支层次结构到顶级功能主干; 然而,懒惰评估从树干到分支提示.渴望没有联合产品("和",a/k/a分类"产品"),懒惰没有析取副产品("或",a/k/a分类"总和")[ 11 ].
性能
急于
与非终止一样,渴望与联合功能组合过于渴望,即组成控制结构做了不必要的工作,而不是懒惰.对于例如,渴望急切和不必要的映射的整个列表,以布尔值,当它是由与第一真实元件上终止的折叠.
这种不必要的工作是所谓的"达到"额外的log n因子的原因,在具有纯函数的渴望与懒惰的连续时间复杂度中.一种解决方案是使用具有惰性构造函数的函子(例如列表)(即,渴望使用可选的惰性产品),因为急切地渴望不正确性来自内部函数.这是因为产品是建设性的类型,即在初始定点上具有初始代数的归纳类型[ 11 ]
懒
与非终止一样,懒惰过于懒惰,具有析取的功能组成,即共同结束可能在必要时间之后发生,导致不必要的工作和迟到的非确定性,而不是急切的情况[ 10 ] [ 11 ] .最终的例子是状态,定时,非终止和运行时异常.这些是必要的副作用,但即使在纯粹的声明性语言(例如Haskell)中,在命令式IO monad中存在状态(注意:并非所有monad都是命令性的!)隐含在空间分配中,而时序是相对于命令式的状态真实世界.即使使用可选的急切副产品,使用延迟也会将"懒惰"泄漏到内部副产品中,因为懒惰的懒惰不正确性来自外部函数(请参阅非终止部分中的示例,其中==是外部二元运算符函数).这是因为副产品受到终结性的限制,即在最终对象上具有最终代数的共同类型[ 11 ].
懒惰导致延迟和空间函数的设计和调试中的不确定性,其调试可能超出了大多数程序员的能力,因为声明的函数层次结构和运行时评估顺序之间存在不一致.使用eager评估的惰性纯函数可能会在运行时引入以前看不见的非终止.相反,使用lazy评估的热切纯函数可能会在运行时引入以前看不见的空间和延迟不确定性.
未结束
在编译时,由于Halting问题和Turing完整语言中的相互递归,通常不能保证函数终止.
急于
对于Head
"和" 的结合Tail
,如果有Head
或Tail
没有终止,则分别为List( Head(), Tail() ).tail == Tail()
或者List( Head(), Tail() ).head == Head()
不是真的,因为左侧没有,而右侧则终止.
然而,懒惰双方终止.因此,渴望对联合产品过于渴望,并且在没有必要的情况下非终止(包括运行时异常).
懒
懒惰但不渴望,对于1
"或" 的分离2
,如果f
不终止,则List( f ? 1 : 2, 3 ).tail == (f ? List( 1, 3 ) : List( 2, 3 )).tail
不是真的,因为左侧终止,而右侧不终止.
然而,由于渴望双方都不会终止所以从未达到过平等测试.因此懒惰对于析取副产品来说太懒惰,并且在那些情况下,在做了比热切的工作更多的工作之后未能终止(包括运行时异常).
[ 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.
对于这些,没有任何非模棱两可的客观定义.我将如何定义它们:
势在必行 -重点是什么步骤,计算机应该不是什么电脑会做(如C,C++,Java的).
声明性 - 重点在于计算机应该做什么而不是应该如何做(例如SQL).
功能 - 声明语言的一个子集,重点关注递归
命令式和陈述式描述了两种相反的编程风格.当务之急是传统的"循序渐进的配方"方法,而声明更多是"这就是我想要的,现在你要弄清楚如何去做".
这两种方法贯穿整个编程 - 即使使用相同的语言和相同的程序.通常,声明性方法被认为是优选的,因为它使程序员不必指定如此多的细节,同时也有更少的机会获得错误(如果你描述了你想要的结果,并且一些经过良好测试的自动过程可以从那个向后工作定义步骤然后你可能希望事情比必须手动指定每个步骤更可靠.
另一方面,一种命令式方法可以让您进行更低级别的控制 - 这是编程的"微管理方法".这可以让程序员利用有关问题的知识来提供更有效的答案.因此,程序的某些部分以更具说明性的方式编写并不罕见,但速度关键部分更为必要.
正如您可能想象的那样,您用来编写程序的语言会影响您的声明性 - 一种内置"智能"的语言,用于计算结果,给出对结果的描述将允许更多的声明程序员需要首先在命令式代码中添加这种智能才能在顶部构建更具声明性的层.因此,例如,像prolog这样的语言被认为是非常具有声明性的,因为它内置了一个搜索答案的过程.
到目前为止,你会注意到我没有提到函数式编程.那是因为它是一个术语,其含义与其他两个词没有直接关系.最简单的函数式编程意味着您可以使用函数.特别是,您使用支持函数作为"第一类值"的语言 - 这意味着您不仅可以编写函数,还可以编写函数来编写函数(编写函数......),并将函数传递给功能.简而言之 - 功能与字符串和数字一样灵活和通用.
然后,通常会一起提到功能性,命令性和声明性.其原因是将功能编程理念"推向极端".从最纯粹的意义上讲,函数是来自数学的东西 - 一种"黑盒子",它接受一些输入并始终提供相同的输出.而且这种行为不需要存储变化的变量.因此,如果你设计一种编程语言,其目的是实现一种非常纯粹的,数学上受影响的函数式编程思想,那么你最终会主要拒绝可以改变的价值观(在一定的,有限的,技术意义上).
如果你这样做 - 如果你限制变量的变化 - 那么几乎是偶然的,你最终迫使程序员编写更具说明性的程序,因为命令式编程的很大一部分是描述变量如何变化,你不能再去做!事实证明,函数式编程 - 特别是函数式编程 - 往往会提供更多的声明性代码.
总结一下,然后:
命令式和声明式是两种相反的编程风格(相同的名称用于鼓励这些风格的编程语言)
函数式编程是一种编程风格,其中函数变得非常重要,因此,改变值变得不那么重要.指定值更改的能力有限会强制使用更具说明性的样式.
所以"函数式编程"通常被描述为"声明式".
简而言之:
一个命令式语言 specfies一系列指令计算机执行序列(这样做,那么做).
一个说明性语言宣布了一组关于应该是什么结果输出从输入(例如,如果你有,那么结果是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)是声明性编程的另一种形式.它涉及代数术语的象征性转换.它完全不同于逻辑编程和函数式编程.
命令式 - 表达式描述要执行的动作序列(关联)
声明性 - 表达式是有助于程序行为的声明(关联,交换,幂等,单调)
功能 -词汇具有值作为唯一的影响; 语义支持等式推理
自从我写完之前的答案以来,我已经制定了一个新的声明性属性定义,引用如下.我还将命令式编程定义为双重属性.
这个定义优于我在之前的答案中提供的定义,因为它简洁而且更通用.但这可能更难以理解,因为适用于编程和生活的不完备性定理的含义对于人类来说难以包裹他们的思想.
引用的定义解释讨论了纯函数式编程在声明性编程中的作用.
所有奇异类型的编程都符合以下声明与命令的分类,因为以下定义声称它们是双重的.
声明与命令
声明性属性是奇怪的,钝的,难以捕获的技术精确定义仍然是一般的而不是模糊的,因为我们可以声明程序的含义(也就是语义)而不会产生意想不到的副作用,这是一个天真的概念.意义表达与避免意外效应之间存在着内在的张力,而这种紧张实际上源于编程和宇宙的不完备性定理.
它过于简单化,技术上不精确,并且通常含糊不清地将声明定义为" 做什么 ",将命令称为" 如何做 ".一个模糊的情况是" 什么 "是输出程序编译器的程序中的" 如何 ".
显然,使语言图灵完成的无界递归也类似于语义 - 不仅在评估的语法结构(又称操作语义)中.这在逻辑上类似于哥德尔定理的一个例子 - " 任何完整的公理系统也是不一致的 ".思考那句话的矛盾怪异!它也是一个例子,演示了语义的表达如何没有可证明的界限,因此我们无法证明2程序(以及类似的语义)也停止了哈尔丁定理.
不完备性定理源于我们宇宙的基本性质,正如热力学第二定律所述," 熵(又称独立可能性的#)趋向于永远最大化 ".程序的编码和设计永远不会finished-它活了! -因为它试图解决一个现实世界的需要,与现实世界的语义总是在不断变化和趋势,以更多的可能性.人类永远不会停止发现新事物(包括程序中的错误;-).
为了在这个没有边缘的奇怪宇宙中精确地和技术上捕捉到上述理想的概念(思考宇宙中没有"外部"),需要一个简洁但看似不那么简单的定义,这个定义听起来不正确,直到它被解释为止.深.
定义:
声明性属性是只存在一组可以表达每个特定模块语义的可能语句的地方.
命令性属性3是双重的,其中语义在组合下是不一致的和/或可以用语句集的变体来表达.
声明性的这种定义在语义范围内具有明显的局部性,这意味着它要求模块化语义保持其一致的含义,无论在何处以及如何在全局范围内实例化和使用它.因此,每个声明性模块语义应该与所有可能的其他语义本质上正交 - 而不是不可能(由于不完整性定理)用于见证一致性的全局算法或模型,这也是Robert Harper教授的" 更多并不总是更好 " 的观点.卡内基梅隆大学计算机科学系,标准ML的设计者之一.
这些模块化声明性语义的实例包括类理论函子例如所述
Applicative
,标称打字,命名空间,命名字段,和WRT到语义的操作水平,那么纯功能的编程.因此,设计良好的声明性语言可以更清楚地表达意义,尽管在可以表达的内容中失去了一般性,但在内在一致性中可以表达的内容却有所增加.
上述定义的一个例子是电子表格程序的单元格中的公式集合 - 当移动到不同的列和行单元格时,预期不会给出相同的含义,即单元格标识符改变.单元标识符是预期含义的一部分,而不是多余的.因此,每个电子表格结果对于一组公式中的单元标识符是唯一的.在这种情况下,一致的模块化语义是使用单元标识符作为单元格公式的纯函数的输入和输出(见下文).
超文本标记语言HTML的又名静态网页pages-语言是一种高度(但不是完全的一个例子3(至少HTML 5之前)没有能力来表达动态行为)说明性语言.HTML可能是最容易学习的语言.对于动态行为,JavaScript等命令式脚本语言通常与HTML结合使用.没有JavaScript的HTML符合声明性定义,因为每个名义类型(即标签)在语法规则内的组合下保持其一致的含义.
声明性的竞争定义是语义语句的可交换和幂等属性,即语句可以在不改变含义的情况下重新排序和复制.例如,如果这些名称是模块化的,则可以重新排序和复制为命名字段分配值的语句,而不会改变程序的含义.名称有时暗示订单,例如单元格标识符包括其列和行位置 - 在电子表格上移动总计更改其含义.否则,这些属性隐式地要求语义的全局一致性.通常不可能设计语句的语义,因此如果随机排序或重复,它们保持一致,因为顺序和重复是语义固有的.例如,语句"Foo exists"(或构造)和"Foo不存在"(和销毁).如果考虑到预期语义的随机不一致性,那么人们接受这个定义对于声明性属性就足够了.从本质上讲,这个定义是一个通用的定义,因为它试图使一致性与语义正交,即无视语义世界是动态无界的,并且无法在全局一致性范式中捕获这一事实.
要求较低级操作语义的(结构评估顺序)的交换和幂等属性将操作语义转换为声明性本地化模块语义,例如纯函数编程(包括递归而不是命令循环).然后,实现细节的操作顺序不会影响(即全局扩展)更高级语义的一致性.例如,电子表格公式的评估顺序(理论上也是重复)无关紧要,因为在计算完所有输出之前,输出不会复制到输入,即类似于纯函数.
C,Java,C++,C#,PHP和JavaScript不是特别声明的.Copute的语法和Python的语法更加声明地耦合到预期的结果,即一致的语法语义,消除了无关的内容,因此人们可以在忘记代码后轻松理解代码.Copute和Haskell强制执行操作语义的确定性,并鼓励" 不要重复自己 "(DRY),因为它们只允许纯粹的功能范式.
2即使我们可以证明程序的语义,例如使用语言Coq,这仅限于在键入时表达的语义,并且键入永远不能捕获程序的所有语义 - 甚至不能用于语言没有图灵完整,例如使用HTML + CSS,可以表达不一致的组合,因此具有未定义的语义.
3许多解释错误地声称只有命令式编程具有语法上有序的语句.我澄清了命令式和函数式编程之间的这种混淆.例如,HTML语句的顺序不会降低其含义的一致性.
编辑:我在Robert Harper的博客上发表了以下评论:
在函数式编程中......变量的变化范围是一种类型
根据人们如何将功能与命令式编程区分开来,命令式程序中的"可分配"也可能具有限制其可变性的类型.
我目前对函数式编程感兴趣的唯一非混乱定义是:a)作为第一类对象和类型的函数,b)对循环递归的偏好,和/或c)纯函数 - 即那些不影响所需语义的函数当被记忆时程序(因此,由于操作语义的影响,例如存储器分配),在通用指称语义中不存在完全纯粹的函数编程.
纯函数的幂等属性意味着对其变量的函数调用可以用其值替换,这通常不是命令式过程的参数的情况.对于输入和结果类型之间的未组合状态转换,纯函数似乎是声明性的.
但是纯函数的组合并不能保持任何这样的一致性,因为可以在纯函数式编程语言中模拟一个副作用(全局状态)命令过程,例如Haskell的IOMonad,而且完全不可能阻止这样做.任何图灵完整的纯函数式编程语言.
正如我在2012年所写的那样,在你最近的博客中似乎有类似的评论共识,声明性编程试图捕捉到预期的语义从不透明的概念.不透明语义的例子是依赖于顺序,依赖于操作语义层的高级语义的擦除(例如,强制转换不是转换,而具体化的泛型限制了更高级别的语义),以及对无法检查的变量值的依赖(证明)正确的)编程语言.
因此,我得出结论,只有非图灵完整语言才能声明.
因此,声明性语言的一个明确且不同的属性可以是其输出可以被证明服从一些可枚举的生成规则集.例如,对于任何特定的HTML程序(忽略口译分歧的方式会有所不同)是没有剧本(即不是图灵完整的),那么它的输出可变性枚举.或者更简洁的是,HTML程序是其可变性的纯函数.同上电子表格程序是其输入变量的纯函数.
所以,在我看来,声明语言的对立面 无限递归,即每哥德尔第二不完备定理的自我指涉的定理不能被证明.
Lesie Lamport 写了一篇童话故事,讲述了欧几里德如何通过类型和逻辑之间的一致性(Curry-Howard对应等)来解决在编程语言语境中应用于数学证明的哥德尔不完备性定理.
命令式编程:告诉"机器"如何做某事,结果你想要发生的事情就会发生.
声明性编程:告诉"机器"你想要发生什么,让计算机弄清楚如何去做.
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); }
注意:区别不在于简洁,复杂或抽象.如上所述,区别在于如何与什么相比.