过程式编程和函数式编程的区别是什么?

我已经阅读了维基百科关于过程式编程函数式编程的文章,但我仍然有点困惑。有人能把它归结为核心吗?

161522 次浏览

在计算机科学中,函数式编程是一种编程范式,它将计算视为数学函数的求值,并避免状态和可变数据。它强调函数的应用,与强调状态变化的过程式编程风格相反。

函数式语言(理想情况下)允许您编写数学函数,即接受n参数并返回值的函数。如果程序被执行,这个函数将根据需要在逻辑上求值

另一方面,过程性语言执行一系列顺序步骤。(有一种方法可以将顺序逻辑转换为函数逻辑,称为延续传球风格。)

因此,纯函数式程序总是为输入产生相同的值,并且求值的顺序没有明确定义;这意味着像用户输入或随机值这样的不确定值很难用纯函数式语言建模。


1正如这个答案中的其他内容一样,这是一个概括。这个属性,在需要计算结果的时候计算,而不是在调用它的时候按顺序计算,被称为“懒惰”。并不是所有的函数式语言都是懒惰的,懒惰也不仅仅局限于函数式编程。相反,这里给出的描述提供了一个“心理框架”,用于思考不同的编程风格,这些风格不是不同的、相反的类别,而是流动的想法。

进一步阐述康拉德的评论:

因此,纯函数式程序总是对输入产生相同的值,求值的顺序没有很好的定义;

因此,函数式代码通常更容易并行化。由于函数(通常)没有副作用,并且它们(通常)只是作用于它们的参数,因此许多并发问题都消失了。

当你需要能够证明时,函数式编程也被使用,你的代码是正确的。这在过程式编程中要困难得多(在函数式编程中不容易,但仍然容易)。

免责声明:我已经很多年没有使用函数式编程了,直到最近才开始重新研究它,所以我在这里可能不完全正确。:)

我相信过程式/函数式/目标式编程是关于如何处理问题的。

第一种风格将所有事情都计划到步骤中,并通过一次实现一个步骤(一个过程)来解决问题。另一方面,函数式编程强调分治方法,将问题划分为子问题,然后解决每个子问题(创建一个函数来解决该子问题),并将结果结合起来创建整个问题的答案。最后,目标编程将通过在计算机内部创建一个包含许多对象的迷你世界来模拟现实世界,每个对象都有(某种程度上)独特的特征,并与其他对象相互作用。从这些相互作用中,结果就会出现。

每种编程风格都有自己的优点和缺点。因此,做一些诸如“纯编程”(即纯粹的程序设计——顺便说一下,没有人会这样做,这有点奇怪——或纯粹的函数式或纯粹的目标)是非常困难的,如果不是不可能的话,除了一些专门设计来展示编程风格优势的基本问题(因此,我们称那些喜欢纯粹的人为“weenie”:D)。

然后,从这些风格中,我们有针对每种风格进行优化的编程语言。例如,Assembly是关于程序的。好吧,大多数早期语言都是过程性的,不只是Asm,像C, Pascal,(还有Fortran,我听说过)。然后,我们在客观学派中都有著名的Java(实际上,Java和c#也在一个叫做“金钱导向”的班级中,但这是另一个讨论的主题)。同样客观的是Smalltalk。在函数学派中,我们会有“近函数”(一些人认为它们是不纯的)Lisp家族和ML家族,以及许多“纯函数”Haskell、Erlang等。顺便说一下,有很多通用语言,如Perl、Python、Ruby。

进一步阐述康拉德的评论:

,求值的顺序不是 良好定义的< / p >

一些函数式语言有所谓的惰性求值。这意味着直到需要该值时才执行函数。在此之前,传递的是函数本身。

过程式语言是步骤1、步骤2、步骤3……如果在第二步你说加2 + 2,它马上就会做。在惰性求值中,你会说2 + 2,但如果结果从未被使用,它就永远不会做加法。

过程性语言倾向于跟踪状态(使用变量),并倾向于按步骤序列执行。纯函数式语言不跟踪状态,使用不可变值,并倾向于作为一系列依赖项执行。在许多情况下,调用堆栈的状态所保存的信息与过程代码中存储在状态变量中的信息相同。

递归是函数式编程的一个经典例子。

基本上这两种风格,就像阴阳。一个是有组织的,而另一个是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么至少有两种语言最近推出了新版本,包含了这两种编程风格。(< a href = " https://raku.org/ " rel = " noreferrer " > Perl 6 < / >和< a href = " https://dlang.org/ " rel = " noreferrer " > D 2 < / >)

#程序:#

  • 例程的输出并不总是与输入直接相关。
  • 每件事都有特定的顺序。
  • 例程的执行可能有副作用。
  • 倾向于强调以线性方式实现解决方案。

# # Perl 6 # #

sub factorial ( UInt:D $n is copy ) returns UInt {


# modify "outside" state
state $call-count++;
# in this case it is rather pointless as
# it can't even be accessed from outside


my $result = 1;


loop ( ; $n > 0 ; $n-- ){


$result *= $n;


}


return $result;
}

# # D 2 # #

int factorial( int n ){


int result = 1;


for( ; n > 0 ; n-- ){
result *= n;
}


return result;
}

#功能:#

  • 通常递归。
  • 对于给定的输入总是返回相同的输出。
  • 计算的顺序通常是不确定的。
  • 必须是无状态的。即任何手术都不能有副作用。
  • 很适合并行执行
  • 倾向于强调分而治之的方法。
  • 可具有惰性求值的特性。
< p > # # Haskell # # (copy from 维基百科)
fac :: Integer -> Integer


fac 0 = 1
fac n | n > 0 = n * fac (n-1)

或者在一行中:

fac n = if n > 0 then n * fac (n-1) else 1

# # Perl 6 # #

proto sub factorial ( UInt:D $n ) returns UInt {*}


multi sub factorial (  0 ) { 1 }
multi sub factorial ( $n ) { $n * samewith $n-1 } # { $n * factorial $n-1 }

# # D 2 # #

pure int factorial( invariant int n ){
if( n <= 1 ){
return 1;
}else{
return n * factorial( n-1 );
}
}

#注:#

阶乘实际上是一个常见的示例,它展示了在Perl 6中创建新的操作符有多么容易,就像创建子例程一样。这个特性在Perl 6中根深蒂固,以至于Rakudo实现中的大多数操作符都是以这种方式定义的。它还允许您将自己的多个候选操作符添加到现有操作符。

sub postfix:< ! > ( UInt:D $n --> UInt )
is tighter(&infix:<*>)
{ [*] 2 .. $n }


say 5!; # 120␤
这个例子还展示了范围创建(2..$n)和列表缩减元操作符([ OPERATOR ] LIST)结合数字中缀乘法操作符。(*)
它还表明你可以在签名中放入--> UInt,而不是后面的returns UInt

(你可以用2作为乘法“运算符”开始范围。在不带任何参数的情况下调用将返回1)

我在这里没有看到真正强调的一点是,现代函数语言(如Haskell)实际上更多地关注流控制的第一类函数,而不是显式递归。您不需要像上面那样在Haskell中递归地定义阶乘。我想是这样的

fac n = foldr (*) 1 [1..n]

是一个完美的惯用结构,在精神上更接近于使用循环,而不是使用显式递归。

康拉德说:

因此,纯函数式程序总是对输入产生相同的值, 评价的顺序也不明确;这意味着不确定的值,比如 用户输入或随机值很难在纯函数式语言中建模

在一个纯函数式程序中求值的顺序可能很难(或者)解释(尤其是懒惰的人),甚至不重要,但我认为说它没有被很好地定义,听起来就像你根本无法判断你的程序是否会工作!

也许更好的解释是,函数式程序中的控制流是基于何时需要函数的参数值。这样做的好处是,在编写良好的程序中,状态变得显式:每个函数将其输入作为参数列出,而不是任意的绿豆全局状态。所以在某种程度上,对一个函数求值的顺序比较容易推断。每个函数可以忽略宇宙的其他部分,专注于它需要做的事情。当组合在一起时,保证函数的工作方式与孤立时相同。

< p >…像用户输入或随机值这样的不确定值很难纯粹地建模 功能语言。< / p >

在纯函数式程序中,输入问题的解决方案是使用一个足够强大的抽象概念将命令式语言嵌入为DSL。在命令式(或非纯函数式)语言中,这是不需要的,因为你可以“欺骗”并隐式传递状态,求值顺序是显式的(不管你喜欢与否)。由于这种“欺骗”和对每个函数的所有参数强制求值,在命令式语言中,1)你失去了创建自己的控制流机制的能力(没有宏),2)代码本质上不是线程安全的和/或可并行的默认情况下, 3)实现像撤消(时间旅行)这样的东西需要仔细工作(命令式程序员必须存储一个获取旧值的方法!),而纯函数式编程可以给你带来所有这些东西,还有一些我可能已经忘记了。“免费”。

我希望这听起来不像狂热,我只是想补充一些观点。命令式编程,特别是像c# 3.0这样的强大语言中的混合范式编程,仍然是完成工作的完全有效的方法和没有灵丹妙药

[1]…除了内存使用方面(参考Haskell中的foldl和foldl')。

@Creighton:

在Haskell中,有一个名为产品的库函数:

prouduct list = foldr 1 (*) list

或者仅仅是:

product = foldr 1 (*)

惯用语的阶乘

fac n = foldr 1 (*)  [1..n]

很简单

fac n = product [1..n]

过程式编程将语句序列和条件构造划分为单独的块,称为过程,这些块通过参数化(非函数式)值。

函数式编程与此类似,只是函数是一类值,因此它们可以作为参数传递给其他函数,并作为函数调用的结果返回。

注意,在这个解释中,函数式编程是过程式编程的泛化。然而,少数人将“函数式编程”解释为没有副作用,这与除Haskell之外的所有主要函数式语言都完全不同,但无关紧要。

如果你有机会,我建议你买一份Lisp/Scheme,然后用它来做一些项目。最近流行起来的大多数思想都是在几十年前用Lisp表达的:函数式编程、延续(作为闭包)、垃圾收集,甚至XML。

所以这将是一个很好的方法来开始所有这些当前的想法,以及一些其他的,比如符号计算。

您应该知道函数式编程擅长什么,不擅长什么。它并不是什么都好。有些问题最好用副作用来表达,同样的问题会根据提问的时间给出不同的答案。

我从来没有在其他地方看到过这样的定义,但我认为这很好地总结了这里给出的差异:

功能编程侧重于表达式

程序上的编程侧重于语句

表达式有值。函数式程序是一个表达式,其值是由计算机执行的一系列指令。

语句没有值,而是修改一些概念机器的状态。

在纯函数式语言中,没有语句,也就是说没有办法操纵状态(它们可能仍然有一个名为“语句”的语法结构,但除非它操纵状态,否则我不会在这种意义上称其为语句)。在纯程序语言中,没有表达式,一切都是操纵机器状态的指令。

Haskell是纯函数式语言的一个例子,因为没有办法操纵状态。机器代码是纯过程语言的一个例子,因为程序中的所有内容都是操作机器寄存器和内存状态的语句。

令人困惑的部分是,绝大多数编程语言都包含这两个表达式和语句,允许您混合范例。语言可以根据它们鼓励使用语句和表达式的程度被分类为更函数化或更过程化。

例如,C比COBOL更具有函数性,因为函数调用是一个表达式,而在COBOL中调用子程序是一个语句(它操纵共享变量的状态,不返回值)。Python将比C语言更实用,因为它允许您将条件逻辑表示为使用短路计算(test &&Path1 || path2,而不是if语句)。Scheme将比Python更具功能性,因为Scheme中的所有内容都是表达式。

你仍然可以在一种鼓励过程范式的语言中以函数式风格编写,反之亦然。只是在语言不鼓励的范式下写作更困难和/或更尴尬。

函数式编程与使用全局变量的过程式编程相同。

这里没有一个答案显示了惯用的函数式编程。递归阶乘的答案很适合在FP中表示递归,但大多数代码不是递归的,所以我不认为这个答案是完全具有代表性的。

假设你有一个字符串数组,每个字符串表示一个整数,比如“5”或“-200”。您希望根据内部测试用例检查这个输入字符串数组(使用整数比较)。两种解决方案如下所示

程序上的

arr_equal(a : [Int], b : [Str]) -> Bool {
if(a.len != b.len) {
return false;
}


bool ret = true;
for( int i = 0; i < a.len /* Optimized with && ret*/; i++ ) {
int a_int = a[i];
int b_int = parseInt(b[i]);
ret &= a_int == b_int;
}
return ret;
}

功能

eq = i, j => i == j # This is usually a built-in
toInt = i => parseInt(i) # Of course, parseInt === toInt here, but this is for visualization


arr_equal(a : [Int], b : [Str]) -> Bool =
zip(a, b.map(toInt)) # Combines into [Int, Int]
.map(eq)
.reduce(true, (i, j) => i && j) # Start with true, and continuously && it with each value

虽然纯函数式语言通常是研究语言(因为现实世界喜欢免费的副作用),但现实世界的过程式语言在适当的时候会使用更简单的函数式语法。

这通常是通过像Lodash这样的外部库实现的,或者是像生锈这样的较新的语言内置的。函数式编程的繁重工作是通过mapfilterreducecurryingpartial这样的函数/概念完成的,你可以查阅其中的后三个来进一步理解。

齿顶高

为了在野外使用,编译器通常必须解决如何在内部将函数式版本转换为过程式版本,因为函数调用开销太高了。递归的情况下,如阶乘显示将使用技巧,如尾部调用删除O(n)内存使用量。没有副作用的事实允许函数式编译器实现&& ret优化,即使.reduce是最后完成的。在JS中使用Lodash,显然不允许任何优化,所以它对性能是一个打击(这通常不是web开发的问题)。像Rust这样的语言将在内部进行优化(并具有诸如try_fold之类的函数来协助&& ret优化)。

要理解其中的区别,我们需要了解过程式编程和函数式编程的“教父”范式都是命令式编程

基本上,过程式编程只是构造命令式程序的一种方式,其中主要的抽象方法是“过程”。(或某些编程语言中的“函数”)。甚至面向对象编程也只是构造命令式程序的另一种方式,其中状态被封装在对象中,成为一个具有“当前状态”的对象,加上这个对象有一组函数、方法和其他东西,可以让程序员操作或更新状态。

现在,关于函数式编程,要点的方法是它确定要取什么值以及这些值应该如何传递。(因此没有状态,也没有可变数据,因为它将函数作为第一类值,并将它们作为参数传递给其他函数)。

PS:理解所使用的每一种编程范式应该能澄清它们之间的差异。

PSS:归根结底,编程范式只是解决问题的不同方法。

PSS: quora的答案有一个很好的解释。

Funtional编程

num = 1
def function_to_add_one(num):
num += 1
return num




function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)
function_to_add_one(num)


#Final Output: 2

过程式编程

num = 1
def procedure_to_add_one():
global num
num += 1
return num




procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()
procedure_to_add_one()


#Final Output: 6

function_to_add_one是一个函数

procedure_to_add_one是一个过程

即使你运行函数 5次,每次它都会返回2

如果你运行过程五次,在第五次运行结束时,它会给你6

免责声明:显然,这是对现实的一种超简化的看法。这个答案只是给了一个“功能”的体验。而不是“程序”。仅此而已。一旦你尝到了这种肤浅而深刻的直觉,开始探索这两种范式,你就会开始清楚地看到它们的区别。

对我的学生有帮助,希望对你们也有帮助。