过程编程和函数式编程有什么区别?[关闭]


246

我已经阅读了有关程序化编程函数式编程的Wikipedia文章,但是我仍然有些困惑。有人可以将其归结为核心吗?


Wikipedia暗示FP是(即始终是)声明式编程的子集,但事实并非如此,它使IP与DP的分类法更加复杂
谢尔比·摩尔三世

Answers:


152

函数式语言(理想情况下)允许您编写数学函数,即采用n个参数并返回值的函数。如果执行程序,则根据需要对该功能进行逻辑评估。1个

另一方面,过程语言执行一系列顺序步骤。(有一种将顺序逻辑转换为功能逻辑的方法,称为连续传递样式。)

结果,一个纯功能的程序对于输入总是产生相同的值,并且评估的顺序没有明确定义。这意味着很难用纯功能语言来建模诸如用户输入或随机值之类的不确定值。


1正如此答案中的所有其他内容一样,这是一个概括。该属性在需要计算结果时而不是在调用结果的位置按顺序进行计算时被称为“惰性”。并非所有的函数语言实际上都是普遍懒惰的,懒惰也不限于函数式编程。相反,此处给出的描述提供了一个“思想框架”,以考虑不同的编程风格,这些风格不是截然相反的类别,而是流动的想法。


9
诸如用户输入或随机值之类的不确定值很难用纯功能语言建模,但这是一个已解决的问题。查看单子。
Apocalisp

顺序步骤,将在其中嵌套功能程序”是指通过强调功能组合(即,将确定性计算的子计算之间的依赖项分离)来提供关注点分离。
谢尔比·摩尔三世

这似乎是错误的-程序也可以嵌套,程序可以有参数
Hurda '16

1
@Hurda是的,本来可以说得更好。关键是程序性编程以预定的顺序逐步进行,而功能性程序则不是逐步执行。而是在需要时计算值。然而,由于缺乏对编程术语的普遍认可的定义,使得这种概括几乎是无用的。在这方面,我已经修改了我的答案。
康拉德·鲁道夫

97

基本上这两种风格,就像阴和阳。一个是有组织的,而另一个则是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么最近至少有两种新版本推出了包含两种编程风格的语言。Perl 6D 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;
}

第2天

int factorial( int n ){

  int result = 1;

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

  return result;
}

功能性:

  • 经常递归。
  • 对于给定的输入,始终返回相同的输出。
  • 评估顺序通常是不确定的。
  • 必须是无状态的。即,任何操作都不会产生副作用。
  • 非常适合并行执行
  • 倾向于强调分而治之的方法。
  • 可能具有“延迟评估”功能。

哈斯克尔

(从Wikipedia复制);

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 }

第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没有任何参数的情况下调用乘号“ operator”将返回)


嗨,您能否考虑一下Perl 6中的析因实现示例,为“过程”中提到的以下两点提供一个示例?1)例程的输出并不总是与输入直接相关。2)执行例程可能会有副作用。
Naga Kiran

sub postfix:<!> ($n) { [*] 1..$n }
布拉德·吉尔伯特

@BradGilbert- No operation can have side effects您能详细说明一下吗?
kushalvm

2
可能是我所能找到的最佳答案。...而且,我对这些要点进行了研究。:)
Navaneeth

1
@AkashBisariya sub foo( $a, $b ){ ($a,$b).pick }←并不总是为相同的输入返回相同的输出,而以下内容会返回sub foo( $a, $b ){ $a + $b }
Brad Gilbert

70

我从未在其他地方看到过这个定义,但是我认为这很好地总结了这里给出的区别:

函数式编程专注于表达式

程序编程侧重于语句

表达式具有值。功能程序是一种表达式,其值是计算机要执行的一系列指令。

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

在纯功能语言中,就没有语句可以操纵状态(它们可能仍具有名为“ statement”的句法构造),在这种意义上,就不会有任何语句,但是除非它操纵状态,否则我不会在这种意义上称其为语句)。在纯粹的过程语言中,不会有表达式,所有内容都会是一条操纵机器状态的指令。

Haskell将是纯功能语言的一个示例,因为无法操纵状态。机器代码将是纯粹过程语言的示例,因为程序中的所有内容都是一条语句,用于操纵机器的寄存器和内存的状态。

混乱的是,绝大多数的编程语言中包含两种表达式和语句,使您可以混合范式。根据语言鼓励使用陈述式还是表达式的程度,可以将它们归类为功能更强或程序性更强的语言。

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

您仍然可以以一种能鼓励程序范式的语言来编写功能样式,反之亦然。用这种语言鼓励的范式编写起来更困难和/或更尴尬。


2
我在网络上看到的最好,最简洁的解释,太棒了!

47

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


4
尽管这是对我帮助最大的解释,但我仍然对函数式编程的概念感到困惑。我正在寻找一种不依赖于引用外部对象即可运行的编程风格(该函数需要运行的所有东西都应作为参数传递)。例如,我永远不会放入GetUserContext()函数中,而会传递用户上下文。这是函数式编程吗?提前致谢。
Matt Cashatt 2014年

26

我相信过程/功能/目标编程是关于如何解决问题的方法。

第一种样式将按步骤计划所有内容,并通过一次执行一个步骤(一个过程)来解决问题。另一方面,函数式编程将强调分而治之的方法,即将问题分为子问题,然后解决每个子问题(创建一个解决该子问题的函数)并将结果组合为为整个问题创造答案。最后,Objective编程将通过在计算机内部创建一个包含许多对象的微型世界来模仿现实世界,每个对象都具有(某种)独特的特征并与其他物体交互。从这些相互作用中将得出结果。

每种编程风格都有其自身的优点和缺点。因此,进行“纯粹编程”之类的事情(即纯粹的程序性-顺便说一句,没有人这样做,这很奇怪-或纯粹的功能性或纯粹的客观性)非常困难,即使不是不可能,除了一些基本问题之外旨在证明编程风格的优势(因此,我们称呼那些喜欢纯粹的人为“ weenie”:D)。

然后,从这些样式中,我们获得了针对每种样式进行优化的编程语言。例如,汇编就是关于程序的。好的,大多数早期语言都是程序性的,不仅像C,Pascal这样的Asm(还有Fortran,我听说)。然后,我们在目标学校都拥有着名的Java(实际上,Java和C#也在名为“面向金钱”的课程中,但这需要进一步讨论)。目标也是Smalltalk。在功能学校中,我们将拥有“几乎功能性”(有些人认为它们是不纯的)Lisp家族和ML家族,还有许多“纯粹功能性”的Haskell,Erlang等。顺便说一下,有许多通用语言,例如Perl,Python ,露比


25

功能编程

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 是一个过程

即使您运行该函数五次,每次也会返回2

如果您运行该过程五次,则在第五次运行结束时将给您6


5
这个例子很容易理解函数式编程中的“无状态”和“不可变数据”,在阅读了这个答案之前,通读上面列出的所有定义和差异并没有消除我的困惑。谢谢!
maximus

13

扩展Konrad的评论:

结果,一个纯粹的功能程序总是为输入产生相同的值,并且评估顺序没有明确定义;

因此,功能代码通常更易于并行化。由于(通常)函数没有副作用,并且(通常)仅对参数起作用,因此许多并发问题都消失了。

当您需要证明能力时,也会使用函数式编程代码正确。使用过程编程很难做到这一点(使用函数不容易,但是仍然更容易)。

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


12

我在这里没有真正强调过的一件事是,像Haskell这样的现代功能语言实际上更多地是关于用于流控制的一流功能,而不是显式递归。您无需像上面一样在Haskell中递归定义阶乘。我觉得像

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

是一个完美的习惯用法,在本质上更类似于使用循环而不是使用显式递归。


10

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


6

程序语言倾向于跟踪状态(使用变量),并倾向于按一系列步骤执行。纯粹的功能语言不会跟踪状态,使用不可变的值,并且倾向于以一系列依赖关系的形式执行。在许多情况下,调用堆栈的状态将保存与程序代码中状态变量中存储的信息等效的信息。

递归是功能样式编程的经典示例。


1
阅读完此页面后,我想到的是同一件事->“递归是函数式编程的经典示例”,您将其清除了。
Mudassir Hussain

6

康拉德说:

结果,一个纯粹的功能程序总是为输入产生相同的值,并且评估顺序没有明确定义;这意味着很难用纯功能语言来建模诸如用户输入或随机值之类的不确定值。

在纯功能程序中,评估的顺序可能很难(尤其是懒惰)甚至不重要,但我认为说它的定义不明确,听起来好像您无法判断程序是否在运行上班!

也许更好的解释是功能程序中的控制流基于何时需要函数参数的值。关于这一点的好消息是,在编写良好的程序中,状态变得很明确:每个函数都将其输入作为参数列出,而不是任意改变全局状态。因此,在某种程度上,更容易一次就一个功能推断评估顺序。每个函数都可以忽略其余部分,而将精力集中在需要做的事情上。组合使用时,可以保证功能与单独运行时的功能相同[1]。

...用纯函数语言很难对诸如用户输入或随机值之类的不确定值进行建模。

在纯功能程序中,输入问题的解决方案是使用足够强大的抽象将命令性语言嵌入DSL。在命令式(或非纯函数式)语言中,这不是必需的,因为您可以“欺骗”并隐式传递状态,并且评估的顺序是明确的(无论您是否喜欢)。由于这种“欺骗”和对所有函数的所有参数的强制求值,使用命令式语言1)您失去了创建自己的控制流机制(没有宏)的能力,2)代码本质上不是线程安全和/或可并行化的默认,3)和实现类似undo(时间旅行)之类的工作需要认真的工作(当务之急,程序员必须存储一个方法来取回旧的值!),而纯函数式编程可以为您提供所有这些东西,而我可能还要已经忘记了“免费”。

我希望这听起来不像是狂热,我只是想补充一些观点。命令式编程,尤其是C#3.0之类的强大语言中的混合范例编程,仍然完成任务的完全有效方法,没有灵丹妙药

[1] ...可能在内存使用方面除外(参见Haskell中的foldl和foldl')。


5

扩展Konrad的评论:

评估顺序不明确

一些功能语言具有所谓的惰性评估。这意味着直到需要该值时才执行功能。在此之前,函数本身就是传递的东西。

程序语言是步骤1步骤2步骤3 ...如果在步骤2中说添加2 + 2,那么它就正确了。在惰性评估中,您会说加2 + 2,但是如果从不使用结果,则永远不会进行加法。


4

如果有机会,我建议您获得一份Lisp / Scheme副本,并在其中进行一些项目。几十年前在Lisp中表达了最近成为潮流的大多数想法:函数式编程,延续(作为闭包),垃圾回收,甚至XML。

因此,这是抢占所有这些当前想法以及其他一些想法(如符号计算)的好方法。

您应该知道函数式编程有什么用处,而哪些不好。这并不适合所有事情。有些问题最好用副作用来表示,其中相同的问题根据提出的时间而给出不同的答案。


3

@Creighton:

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

prouduct list = foldr 1 (*) list

或者简单地:

product = foldr 1 (*)

所以“惯用”阶乘

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

简直是

fac n = product [1..n]

这不能为问题提供答案。要批评或要求作者澄清,请在其帖子下方发表评论。
尼克·基托

我相信这是在多年前添加评论系统之前发布的,如果您可以相信的话:stackoverflow.com/help/badges/30/beta?
userid=2543

2

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

除了函数是一等值之外,函数式编程是相同的,因此它们可以作为参数传递给其他函数,并作为函数调用的结果返回。

请注意,在这种解释中,函数式编程是过程编程的概括。但是,少数人将“函数式编程”解释为无副作用,这完全不同,但与除Haskell以外的所有主要函数式语言无关。


1

要理解这种差异,需要了解程序和功能编程的“教父”范式是命令式编程

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

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

PS:了解每种编程范例所用的内容,应阐明它们之间的差异。

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

PSS:这个法定答案有很好的解释。


0

这里没有答案显示惯用的函数式编程。递归阶乘答案非常适合表示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)或可用的内置内建语言(如Rust)来实现。函数式编程的繁重与类似功能/概念做了mapfilterreducecurryingpartial,它的最后三个你可以看一下做进一步的了解。

附录

为了在野外使用,编译器通常必须弄清楚如何在内部将功能版本转换为过程版本,因为函数调用开销太高。递归情况(例如所示的阶乘)将使用诸如尾调用之类的技巧来删除O(n)内存使用情况。没有副作用的事实允许函数式编译器&& ret即使在.reduce最后完成时也可以实现优化。在JS中使用Lodash显然不允许进行任何优化,因此会降低性能(通常与Web开发无关)。诸如Rust之类的语言将在内部进行优化(并具有诸如try_fold辅助&& ret优化之类的功能)。

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.