程序编程和函数编程有什么区别?

时间:2008-08-22 19:29:45

标签: functional-programming glossary paradigms procedural-programming

我已经阅读了procedural programmingfunctional programming的维基百科文章,但我仍然有些困惑。有人可以把它归结为核心吗?

17 个答案:

答案 0 :(得分:124)

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

另一方面,过程语言执行一系列顺序步骤。 (有一种方法可以将顺序逻辑转换为名为continuation passing style的功能逻辑。)

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


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

答案 1 :(得分:88)

基本上这两种风格,都喜欢阴阳。一个是有组织的,而另一个是混乱的。有些情况下功能编程是明显的选择,其他情况是程序编程是更好的选择。这就是为什么至少有两种语言最近推出了新版本,它包含了两种编程风格。 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;
}

D 2

int factorial( int n ){

  int result = 1;

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

  return result;
}

功能

  • 通常是递归的。
  • 始终为给定输入返回相同的输出。
  • 评估顺序通常未定义。
  • 必须是无国籍人。即没有手术会产生副作用。
  • 非常适合并行执行
  • 倾向于强调分而治之的方法。
  • 可能具有Lazy Evaluation功能。

Haskell

(从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 }

D 2

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

旁注:

Factorial实际上是一个常见的例子,表明在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

答案 2 :(得分:57)

我从未在其他地方看到这个定义,但我认为这总结了这里给出的差异:

功能性编程侧重于表达式

程序性编程侧重于语句

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

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

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

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

令人困惑的部分是绝大多数编程语言包含两个表达式和语句,允许您混合范例。语言可以根据他们鼓励使用语句与表达的程度来分​​类为更多功能或更多程序。

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

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

答案 3 :(得分:43)

在计算机科学中,函数式编程是一种编程范式,它将计算视为数学函数的评估,并避免状态和可变数据。它强调功能的应用,与强调状态变化的程序编程风格形成对比。

答案 4 :(得分:25)

我认为程序/功能/目标编程是关于如何处理问题的。

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

每种编程风格都有自己的优点和缺点。因此,做一些诸如“纯编程”(即纯粹的程序性 - 没有人这样做,顺便说一下,有点奇怪 - 或纯粹的功能或纯粹的客观)是非常困难的,如果不是不可能的话,除了一些特殊的基本问题旨在展示编程风格的优势(因此,我们称那些喜欢纯粹的人为“weenie”:D)。

然后,从这些样式中,我们有编程语言,旨在针对每种样式进行优化。例如,汇编就是程序性的。好吧,大多数早期语言都是程序性的,不仅仅是Asm,比如C,Pascal,(和Fortran,我听说过)。然后,我们在客观学校中拥有所有着名的Java(实际上,Java和C#也属于一个名为“以金钱为导向”的课程,但这是另一个讨论的主题)。 Smalltalk也是客观的。在功能学校,我们会有“近乎功能性”(有些人认为它们是不纯的)Lisp家族和ML家族以及许多“纯粹功能性”的Haskell,Erlang等。顺便说一下,有许多通用语言,如Perl,Python ,Ruby。

答案 5 :(得分:12)

扩展康拉德的评论:

  

因此,纯函数式程序总是为输入生成相同的值,并且评估的顺序没有明确定义;

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

当您需要能够证明您的代码是正确的时,也会使用函数式编程。这对程序编程来说要困难得多(功能不容易,但更容易)。

免责声明:我多年没有使用过函数式编程,而且最近才重新开始使用它,所以我在这里可能不完全正确。 :)

答案 6 :(得分:11)

我在这里没有看到的一件事是,现代函数语言(如Haskell)在流控制的第一类函数上比在显式递归中更多。您不需要在Haskell中递归地定义阶乘,如上所述。我觉得像

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

是一个完美的惯用结构,在使用循环方面比使用显式递归更接近精神。

答案 7 :(得分:7)

函数式编程与过程式编程相同,其中全局变量正在使用。

答案 8 :(得分:6)

康拉德说:

  

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

纯粹功能性程序中的评估顺序可能很难(呃)推理(特别是懒惰)甚至不重要但我认为说它没有明确定义会让你觉得你不知道你的该计划将全力以赴!

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

  

...用户输入或随机值等不确定值很难完全模拟     功能语言。

纯功能程序中输入问题的解决方案是使用DSL将命令式语言嵌入a sufficiently powerful abstraction。在命令式(或非纯函数式)语言中,这不是必需的,因为您可以“欺骗”并隐式传递状态,并且评估的顺序是明确的(无论您是否喜欢)。由于这种“作弊”并强制评估每个函数的所有参数,在命令式语言1)你失去了创建自己的控制流机制(没有宏)的能力,2)代码本身并不是线程安全的和/或可并行化的默认情况下,3)并实现像撤消(时间旅行)之类的东西需要仔细的工作(命令式程序员必须存储一个用于获取旧值的配方!),而纯函数式编程会购买所有这些东西 - 还有一些我可能已经忘记了 - “免费”。

我希望这听起来不像狂热,我只想添加一些观点。命令式编程,尤其是C#3.0等强大语言中的混合范式编程仍然是完成任务的完全有效的方法there is no silver bullet

[1] ...除了可能尊重内存使用(参见Haskell中的foldl和foldl')。

答案 9 :(得分:6)

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

递归是功能样式编程的典型例子。

答案 10 :(得分:5)

扩展康拉德的评论:

  

并且评估顺序不是   良好定义

一些函数式语言具有所谓的Lazy Evaluation。这意味着在需要值之前不会执行函数。在此之前,函数本身就是传递的东西。

程序语言是步骤1步骤2步骤3 ...如果在步骤2中你说加2 + 2,它就是正确的。在懒惰的评估中,你会说添加2 + 2,但如果结果从未使用过,它就不会添加。

答案 11 :(得分:4)

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

因此,这将是一个很好的方式来开始所有这些当前的想法,还有一些,如符号计算。

你应该知道什么是函数式编程,哪些函数编程不好。这对一切都不好。有些问题最好用副作用来表达,同样的问题根据被问及的时间给出不同的答案。

答案 12 :(得分:3)

@Creighton:

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

prouduct list = foldr 1 (*) list

或简单地说:

product = foldr 1 (*)

所以“惯​​用”因子

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

只是

fac n = product [1..n]

答案 13 :(得分:3)

功能性编程

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

答案 14 :(得分:2)

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

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

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

答案 15 :(得分:1)

要了解两者之间的差异,需要了解程序和函数式编程的“教父”范式是命令式编程

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

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

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

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

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

答案 16 :(得分: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之类的函数/概念来完成的,您可以选择其中的最后三个寻找进一步的了解。

附录

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