功能性,声明性和命令式编程
术语功能性,声明性和命令性编程是什么意思?
在写这篇文章的时候,这个页面上的顶部投票答案在声明式和命令式的定义上是不精确和混乱的,包括引用维基百科的答案。 一些答案以不同的方式混合了术语。
无论公式如何变更单元格,还请参阅我为何解释电子表格程序的声明。
另外,有几个答案声称函数式编程必须是声明式的一个子集。 在这一点上,这取决于我们是否将“功能”与“程序”区分开来。 让我们首先处理命令式和声明式。
声明式表达的定义
唯一可以区分声明式表达式和命令式表达式的属性是其子表达式的参考透明度(RT)。 所有其他属性或者在两种表达式之间共享,或者从RT派生。
100%声明性语言(即其中每个可能表达式都是RT的语言)不会(在其他RT需求中)允许存储值的变化,例如HTML和Haskell的大部分。
RT表达的定义
RT通常被称为“无副作用”。 术语效应没有一个确切的定义,所以有些人不同意“无副作用”与RT相同。 RT有一个精确的定义。
由于每个子表达式在概念上都是一个函数调用,因此RT要求函数(即被调用函数内部的表达式)的实现可能不会访问函数外部的可变状态(访问可变局部状态为允许)。 简而言之,函数(实现)应该是纯粹的。
纯函数的定义
一个纯粹的功能通常被认为是“没有副作用”。 术语效果没有明确的定义,所以有些人不同意。
纯函数具有以下属性。
请记住,RT适用于表达式(其中包括函数调用),纯度适用于函数的(实现)。
制作RT表达式的不纯功能的一个不为人知的例子是并发性,但这是因为中断抽象层的纯度被破坏了。 你并不需要知道这一点。 要制作RT表达式,可以调用纯函数。
RT的衍生属性
为声明性编程引用的任何其他属性,例如维基百科使用的1999年的引用,都来自RT,或与命令式编程共享。 从而证明我的确切定义是正确的。
请注意,外部值的不变性是RT要求的一个子集。
声明性语言没有循环控制结构,例如for
和while
,因为由于不变性 ,循环条件永远不会改变。
声明性语言除了嵌套函数顺序(又称逻辑依赖性)之外不表示控制流,因为由于不可变性 ,其他评估顺序选择不会改变结果(见下文)。
声明性语言表达逻辑“步骤”(即嵌套的RT函数调用顺序),但是每个函数调用是否是更高级别的语义(即“做什么”)不是声明性编程的要求。 与命令的区别在于, 由于不变性 (即更一般的RT),这些“步骤”不能取决于可变状态,而仅取决于所表达的逻辑的关系顺序(即函数调用的嵌套顺序,也就是子表达式)。
例如,只有在段落中的子表达式(即标签)被评估之后,才能显示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 (gab) (hcd)
。
评估订单详情
函数是从输入到输出的状态转换(不是可变的存储值)。 对于调用纯函数的RT组合,这些状态转换的执行顺序是独立的。 每个函数调用的状态转换与其他函数调用的状态转换相互独立,因为缺乏副作用以及RT函数可能被其缓存值替换的原则。 为了纠正一个流行的误解,尽管Haskell的IO
monad可以说是不纯的,并且对于程序外部的World
状态也是必不可少的,但是纯粹的monadic构成总是声明式和RT ,但是从下面的警告意义上讲, - 影响是孤立的)。
急切的评估意味着函数参数在函数被调用之前被评估,懒惰评估意味着直到(和如果)它们在函数内被访问,参数才被评估。
定义 :函数参数在函数定义站点声明,函数参数在函数调用站点提供。 知道参数和参数之间的区别。
从概念上讲,所有表达式(的组合物)的函数调用,例如常数不带输入功能,一元运算符是具有一个输入功能,二进制中缀运算符是具有两个输入端的功能,构造函数的功能,甚至控制语句(例如if
, for
, while
)可以用功能建模。 这些参数的函数(不使用嵌套函数调用为了混淆)的评估不是由语法,如声明的顺序f( g() )
可能急切地评估g
然后f
上g
的结果,也可能评估f
只有在f
内需要结果时懒散地评估g
。
警告,没有图灵完整的语言(即允许无限递归)是完全的陈述性的,例如懒惰评估引入了记忆和时间不确定性。 但是,由于评估顺序的选择,这些副作用仅限于内存消耗,执行时间,延迟,非终止以及外部滞后,从而导致外部同步。
功能编程
因为声明式编程不能有循环,所以迭代的唯一方法就是函数递归。 从这个意义上说,函数式编程与声明式编程有关。
但函数式编程不限于声明式编程 。 功能组成可以与子类型相对照,特别是关于表达问题,其中扩展可以通过添加子类型或功能分解来实现。 扩展可以是两种方法的混合。
函数式编程通常使函数成为第一类对象,这意味着函数类型可以出现在任何其他类型的语法中。 结果是功能可以输入和操作功能,从而通过强调功能组合来分离关注点,即分离确定性计算的子计算之间的依赖关系。
例如,对于可以应用于集合中每个元素的无数个可能的专用操作中的每一个,编写独立的函数(并且如果该函数也必须是声明性的,则使用递归而不是循环),函数式编程使用可重复使用的迭代功能,例如map
, fold
, filter
。 这些迭代函数输入一个一流的专用动作函数。 这些迭代函数迭代集合并为每个元素调用输入专用操作函数。 这些动作函数更简洁,因为它们不再需要包含循环语句来迭代集合。
但是请注意,如果函数不是纯粹的,那么它确实是一个过程。 我们也许可以争辩说,使用不纯功能的函数式编程实际上是程序编程。 因此,如果我们同意声明式表达式是RT,那么我们可以说过程式编程不是声明式编程,因此我们可能会认为函数式编程总是RT,并且必须是声明式编程的子集。
排比
具有一流功能的这种功能组合可以通过分离独立功能来表达平行度的深度。
布伦特原理:工作w和深度d的计算可以在时间O(max(w / p,d))中的p处理器PRAM中实现。
并发性和并行性都需要声明性编程,即不可变性和RT。
那么Parallelism == Concurrency从何而来的危险假设呢? 这是带有副作用的语言的自然结果:当你的语言在各处都有副作用时,任何时候当你尝试做多件事情时,你基本上都会由于每次操作产生的交叉影响而导致非确定性。 所以在副作用语言中,获得并行性的唯一方法是并发性; 因此我们经常看到两者混淆并不奇怪。
FP评估顺序
请注意,评估顺序也会影响功能组合的终止和性能副作用。
渴望(CBV)和懒惰(CBN)是类别决斗[10],因为它们颠倒了评估顺序,即分别评估外部函数还是内部函数。 想象一个颠倒的树,然后渴望从功能树分支中评估分支层次结构中的顶层功能树干; 而懒惰评估从树干到分支提示。 Eager没有连接产品(“和”,a / k / a分类“产品”),懒惰没有分离的副产品(“或”,a / k / a分类“总和”)[11]。
性能
急于
与非终止一样,渴望过于渴望联结的功能组成,即组成控制结构做不必要的工作,而不是懒惰的工作。 例如,当它由一个以第一个真实元素结尾的折叠组成时,急切地和不必要地将整个列表映射到布尔值。
这种不必要的工作是声称“高达”一个额外的日志因素在渴望与懒惰的连续时间复杂性的原因,都与纯函数。 一种解决方案是使用函子(例如列表)和懒惰的构造函数(即渴望使用可选的懒惰产品),因为急切的渴望不正确源于内部函数。 这是因为产品是建设性的类型,即初始代数在初始定点上的归纳类型[11]
懒
与非终止一样,懒惰对于分离功能组成来说太懒惰,即共诱导终结可能晚于必要发生,导致迟到的不必要的工作和非确定性,而不是急于求助[10] [11] 。 终结点的例子是状态,定时,非终止和运行时异常。 这些都是必要的副作用,但即使是纯粹的声明性语言(例如Haskell),在命令式IO monad中也存在状态(注意:并非所有monad都是必要的!)隐含在空间分配中,并且时序是相对于命令的状态真实世界。 因为懒惰懒惰的不正确来自外部函数(参见非终止部分中的示例,其中==是外部二元运算符函数),所以使用懒惰即使对于可选的保留副本产品也会将“懒惰”泄漏到内部副本产品中。 这是因为副产品受终结限制,即共诱导类型与最终代数上的最终代数[11]。
由于所声明的函数层次结构与运行时评估顺序之间的不一致,因此懒惰会导致对延迟和空间函数的设计和调试产生不确定性,其调试可能超出大多数程序员的能力。 惰性计算的懒惰纯函数可能潜在地在运行时引入先前未见过的非终止。 相反,懒惰评估的渴望纯函数可能会潜在地在运行时引入先前看不见的空间和延迟不确定性。
未结束
在编译时,由于Haling问题和图灵完备语言中的相互递归,函数通常不能保证终止。
急于
对于Head
和Tail
的结合,如果Head
或Tail
不结束,则分别使用List( Head(), Tail() ).tail == Tail()
或List( Head(), Tail() ).head == Head()
不是真的,因为左侧没有,右侧是终止。
而懒惰双方终止。 因此,渴望对结合产品过于渴望,并且在没有必要的情况下不终止(包括运行时异常)。
懒
懒惰但不渴望,对于1
或2
List( f ? 1 : 2, 3 ).tail == (f ? List( 1, 3 ) : List( 2, 3 )).tail
,如果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 SCL中的CBV和CBN。
[11]声明性延续和分类对偶,菲林斯基,2.2.1节产品和副产品,2.2.2终端和初始对象,2.5.2 CBV带有惰性产品,2.5.3 CBN带有热切副产品。
这些没有任何非模棱两可的客观定义。 以下是我如何定义它们:
势在必行 - 重点在于计算机应采取的步骤,而不是计算机将执行的操作(例如C,C ++,Java)。
声明式 - 重点在于计算机应该做什么,而不是它应该如何去做(例如SQL)。
功能性 - 重点关注递归的声明性语言的子集
命令式和陈述式描述两种相反的编程风格。 势在必行的是传统的“逐步配方”方法,而陈述式更多的是“这就是我想要的,现在你正在研究如何去做”。
这两种方法在整个编程过程中都会发生 - 即使使用相同的语言和相同的程序。 通常声明式方法被认为是可取的,因为它使程序员不必指定如此多的细节,同时也减少了错误的可能性(如果你描述了你想要的结果,而且一些经过充分测试的自动化过程可以从那里向后工作定义步骤,那么你可能希望事情比必须手动指定每个步骤更可靠)。
另一方面,一种必要的方法可以让你获得更低水平的控制 - 这是编程的“微观管理方法”。 这可以让程序员利用有关问题的知识来提供更有效的答案。 所以一个程序的某些部分被写成更具说明性的风格并不罕见,但对于速度至关重要的部分则更为重要。
正如你可能想象的那样,你用来编写程序的语言会影响你如何声明 - 一种内置“智能”的语言用于制定如何做结果描述,这将允许更多的声明而不是程序员需要首先在命令式代码中添加这种智能的方法,然后才能在顶层构建一个更具说明性的层。 例如,像prolog这样的语言被认为是非常具有说明性的,因为它内置了一个搜索答案的过程。
到目前为止,你会注意到我没有提到函数式编程。 那是因为这是一个术语,其含义并不直接与另外两个相关。 在其最简单的功能编程意味着你使用函数。 特别是你使用支持函数的语言作为“第一类值” - 这意味着你不仅可以编写函数,而且可以编写写函数的函数(写函数......),并将函数传递给功能。 简而言之 - 这些功能像字符串和数字一样灵活和通用。
这似乎很奇怪,那么,功能性,命令性和陈述性就经常被一起提及。 其原因是将功能性编程思想“推向极致”的结果。 从最纯粹的意义上说,函数是数学的东西 - 一种需要一些输入并始终给出相同输出的“黑匣子”。 这种行为不需要存储变化的变量。 所以如果你设计的编程语言的目标是实现一个非常纯粹的,数学上受到数学影响的函数式编程思想,那么你最终会拒绝(可能会在某种有限的技术意义上)改变值的想法。
如果你这样做 - 如果你限制变量如何改变 - 那么你几乎无意中最终迫使程序员编写更具说明性的程序,因为大部分命令式编程描述变量如何变化,而且你不能再去做! 所以事实证明,函数式编程 - 特别是用函数式语言编程 - 往往会提供更多的声明式代码。
总结一下,那么:
命令式和声明式是两种相反的编程风格(相同的名称用于鼓励这些风格的编程语言)
函数式编程是一种编程风格,函数变得非常重要,因此,变化的值变得不那么重要。 指定值更改的能力有限会强制更具说明性的风格。
所以“函数式编程”通常被描述为“声明式”。
链接地址: http://www.djcxy.com/p/3911.html