真正了解程序和功能之间的区别


114

我真的很难理解过程式函数式编程范式之间的区别。

这是Wikipedia条目中有关函数式编程的前两段:

在计算机科学中,函数式编程是一种编程范例,将计算视为对数学函数的评估,并避免了状态和可变数据。与强调状态变化的命令式编程风格相反,它强调函数的应用。函数式编程起源于lambda演算,lambda演算是1930年代开发的用于研究函数定义,函数应用程序和递归的正式系统。许多函数式编程语言可以看作是lambda演算的精妙之处。

实际上,数学函数和命令式编程中使用的“函数”概念之间的区别在于命令式函数可能会产生副作用,从而改变程序状态的值。因此,它们缺乏参照透明性,即,相同的语言表达可能会在不同时间根据执行程序的状态产生不同的值。相反,在功能代码中,函数的输出值仅取决于输入到该函数的参数,因此f两次调用 具有相同参数值的函数x会产生相同的结果f(x)两次。消除副作用可以使理解和预测程序的行为变得更加容易,这是开发函数式编程的主要动机之一。

在第2段中,

相反,在功能代码中,函数的输出值仅取决于输入到函数的参数,因此f两次调用具有相同参数值的函数x会产生相同的结果f(x)

程序编程不是完全一样吗?

在程序与功能上脱颖而出应该寻找什么?


1
Abafei的“迷人的Python:Python中的函数式编程”链接被打破。这是一个很好的链接集:ibm.com/developerworks/linux/library/l-prog/index.html ibm.com/developerworks/linux/library/l-prog2/index.html
Chris Koknat 2012年

命名的另一个方面。例如。在JavaScript和Common Lisp中,即使有副作用,我们也使用函数一词;在Scheme中,该术语始终称为过程。可以将纯CL函数编写为纯函数Scheme过程。几乎所有有关Scheme的书都使用术语过程,因为它是标准中使用的术语,与它的过程性或功能性无关。
Sylwester

Answers:


276

功能编程

函数式编程是指将函数视为值的能力。

让我们考虑一个具有“常规”值的类比。我们可以取两个整数值,并使用+运算符将它们合并以获得一个新的整数。或者,我们可以将整数乘以浮点数以获得浮点数。

在函数式编程中,我们可以使用composelift运算符将两个函数值组合以产生新的函数值。或者,我们可以使用mapfold这样的运算符将函数值和数据值结合起来以产生新的数据值。

请注意,许多语言都具有功能编程功能-甚至是通常不被认为是功能语言的语言。即使祖父FORTRAN也支持函数值,尽管它在函数组合运算符方面没有提供太多帮助。对于一种称为“功能性”的语言,它需要大范围地包含功能性编程功能。

程序编程

程序性编程是指将通用的指令序列封装到过程中的能力,以便可以在不使用复制粘贴的情况下从许多地方调用这些指令。由于程序是编程的早期发展,因此功能几乎总是与机器语言或汇编语言编程所要求的编程风格相关联:一种强调存储位置和在这些位置之间移动数据的指令的风格。

对比

两种风格并非真正相反-它们只是彼此不同。有些语言完全包含两种样式(例如LISP)。下面的情况可能会使您感觉这两种样式有所不同。让我们为一个无意义的要求写一些代码,我们要确定列表中的所有单词是否都具有奇数个字符。一,程序风格:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

鉴于此示例是可以理解的,因此我认为。现在,功能风格:

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

从内到外,此定义执行以下操作:

  1. compose(odd, length)组合oddlength函数以产生一个新函数,该函数确定字符串的长度是否为奇数。
  2. map(..., words)为中的每个元素调用该新函数words,最终返回一个新的布尔值列表,每个布尔值指示相应的单词是否具有奇数个字符。
  3. apply(and, ...)将“和”运算符应用于结果列表,然后将所有布尔值加在一起以产生最终结果。

从这些示例中可以看到,过程编程非常关心在变量中移动值并显式描述产生最终结果所需的操作。相反,功能样式强调将初始输入转换为最终输出所需的功能组合。

该示例还显示了过程代码与功能代码的典型相对大小。此外,它证明了过程代码的性能特征可能比功能代码的特征更容易看清。考虑:这些函数是否计算列表中所有单词的长度,或者每个函数在找到第一个偶数长度的单词后立即停止?另一方面,功能代码允许高质量的实现执行一些相当认真的优化,因为它主要表示意图而不是显式算法。

进一步阅读

这个问题经常出现……例如:

John Backus的Turing奖演讲非常详细地阐明了函数式编程的动机:

可以从冯·诺依曼风格中解放编程吗?

在当前情况下,我真的不应该提及该论文,因为它很快就变得技术性很强。我无法抗拒,因为我认为它确实是基础。


附录-2013

评论员指出,流行的当代语言提供了除程序和功能之外的其他编程风格。此类语言通常提供以下一种或多种编程样式:

  • 查询(例如列表理解,语言集成查询)
  • 数据流(例如隐式迭代,批量操作)
  • 面向对象(例如封装的数据和方法)
  • 面向语言(例如,特定于应用程序的语法,宏)

有关此响应中的伪代码示例如何从其他样式可用的某些功能中受益的示例,请参见以下注释。特别是,该过程示例将从几乎任何更高级别的构造的应用中受益。

展示的示例故意避免混用其他编程风格,以强调所讨论的两种风格之间的区别。


1
确实是个不错的答案,但是您可以简化一下代码吗,例如:“ function allOdd(words){foreach(单词中的自动单词){奇数(length(word)?return false:;} return true;}”)
Dainius

与python中的“功能样式”相比,那里的功能样式非常难读:defodd_words(words):返回[x为x,如果单词为奇数(len(x(x)))]
装箱

@boxed:您的odd_words(words)定义所做的事情与答案的不同allOdd。对于过滤和映射,通常首选列表理解,但此处应将此功能allOdd简化为单个布尔值的单词列表。
ShinNoNoir

@WReach:我会像这样编写您的函数示例:function allOdd(words){return and(odd(length(first(words))),allOdd(rest(words))); }它不比您的示例优雅,但是在尾部递归语言中,它将具有与命令式样式相同的性能特征。
mishoo 2013年

我相信,@ mishoo语言必须在and运算符中同时具有尾递归性和严格性,并且必须短路。
kqr

46

函数式和命令式编程之间的真正区别是心态-命令式程序员在考虑变量和内存块,而函数式程序员在思考“如何输入数据转换为输出数据”-您的“程序”就是管道以及对数据的一组转换,以将其从输入转换为输出。这是IMO有趣的部分,而不是“您不应该使用变量”位。

由于这种心态的结果,FP程序通常描述什么会发生,而不是具体机制如何它会发生-这是强大的,因为如果我们能清楚地说明什么是“选择”和“去哪儿”和“聚合”的意思,我们可以自由地交换其实现,就像我们使用AsParallel()一样,突然,我们的单线程应用程序可以扩展到n个核心。


您可以使用代码示例片段将两者进行对比吗?真正欣赏它
Philoxopher 2011年

1
@KerxPhilo:这是一个非常简单的任务(将数字从1添加到n)。当务之急:修改当前数字,到目前为止修改总和。代码:int i,总和;总和= 0; 对于(i = 1; i <= n; i ++){sum + = i; }。实用(Haskell):懒惰地列出数字,将它们折叠在一起,同时加零。代码:foldl(+)0 [1..n]。抱歉,注释中没有格式。
2014年

为答案+1。换句话说,函数式编程是指尽可能编写无副作用的函数,即,在给定相同参数的情况下,函数总是返回相同的东西-这就是基础。如果您遵循这种方法,那么副作用(您始终需要它们)将被隔离,其余功能仅将输入数据转换为输出数据。
beluchin

12
     Isn't that the same exact case for procedural programming?

不可以,因为程序代码可能会有副作用。例如,它可以存储呼叫之间的状态。

也就是说,可以使用程序语言来编写满足此约束的代码。而且,也有可能用某些被认为是功能性的语言编写打破这种约束的代码。


1
您可以举一个例子和比较吗?如果可以的话,请真正感谢。
Philoxopher 2011年

8
C语言中的rand()函数为每个调用提供不同的结果。它存储两次调用之间的状态。它不是参照透明的。相比之下,C ++中的std :: max(a,b)总是在给定相同参数的情况下返回相同的结果,并且没有副作用(我知道...)。
安迪·托马斯

11

我不同意WReach的回答。让我们对他的回答进行一些解构,看看分歧来自何处。

首先,他的代码:

function allOdd(words) {
  var result = true;
  for (var i = 0; i < length(words); ++i) {
    var len = length(words[i]);
    if (!odd(len)) {
      result = false;
      break;
    }
  }
  return result;
}

function allOdd(words) {
  return apply(and, map(compose(odd, length), words));
}

要注意的第一件事是他正在混合:

  • 功能性
  • 面向表达和
  • 以迭代器为中心

编程,并且缺少迭代样式编程比典型功能样式具有更明确的控制流的功能。

让我们快速谈谈这些。

以表达为中心的样式是事物尽可能多地评估事物的一种方式。尽管函数式语言因对表达式的热爱而闻名,但实际上可能会有一种没有可组合表达式的函数式语言。我要弥补一个问题,没有表达式,只有语句。

lengths: map words length
each_odd: map lengths odd
all_odd: reduce each_odd and

这与以前给出的几乎相同,除了函数完全通过语句和绑定链进行链接。

以迭代器为中心的编程风格可能是Python采取的一种。让我们使用迭代的,以迭代器为中心的样式:

def all_odd(words):
    lengths = (len(word) for word in words)
    each_odd = (odd(length) for length in lengths)
    return all(each_odd)

这是不起作用的,因为每个子句都是一个迭代过程,并且它们通过堆栈帧的显式暂停和恢复而绑定在一起。语法可能会部分地从功能语言中获得启发,但会应用于其完全迭代的实施方式。

当然,您可以压缩以下内容:

def all_odd(words):
    return all(odd(len(word)) for word in words)

命令性现在看起来还不错,是吗?:)

最后一点是关于更明确的控制流程。让我们重写原始代码以使用此代码:

function allOdd(words) {
    for (var i = 0; i < length(words); ++i) {
        if (!odd(length(words[i]))) {
            return false;
        }
    }
    return true;
}

使用迭代器,您可以:

function allOdd(words) {
    for (word : words) { if (!odd(length(word))) { return false; } }
    return true;
}

那么,什么功能性语言的点,如果偏差在:

return all(odd(len(word)) for word in words)
return apply(and, map(compose(odd, length), words))
for (word : words) { if (!odd(length(word))) { return false; } }
return true;


函数式编程语言的主要定义特征是,它消除了作为典型编程模型一部分的突变。人们通常认为这是一种功能性编程语言没有语句或不使用表达式,但这只是简化。功能语言用行为声明代替显式计算,然后对语言进行简化。

将自己限制在此功能子集中,可以使您对程序的行为有更多的保证,并且可以更自由地编写它们。

使用功能语言时,制作新功能通常与编写紧密相关的功能一样简单。

all = partial(apply, and)

如果您尚未显式控制函数的全局依赖关系,这将不简单,甚至可能无法实现。函数式编程的最大特点是,您可以一致地创建更多的通用抽象,并相信可以将它们组合成更大的整体。


您知道,尽管我同意拥有非常通用的算法的出色能力,但我敢肯定a apply的操作与a fold或操作不太相同reduce
本尼迪克特·李

我从没听说过apply要表示foldreduce,但是在我看来,它必须在这种情况下才能返回布尔值。
Veedrac 2014年

嗯,好的,我对命名感到困惑。感谢您清理它。
本尼迪克特·李

6

在过程范式中(我应该说“结构化编程”吗?),您共享了可变存储器和以某种顺序(一个接一个)读写的指令。

在函数范式中,您具有变量和函数(从数学意义上说:变量不会随时间变化,函数只能根据其输入来计算某些内容)。

(这被过分简化了,例如,FPL通常具有用于可变存储器的功能,而过程语言通常可以支持高阶过程,因此事情并不那么清晰;但这应该可以给您一个思路)



2

在函数式编程中,为了推理符号的含义(变量或函数名称),您实际上只需要知道两件事-当前作用域和符号名称。如果您使用的是纯功能性语言且具有不变性,那么这两个都是“静态的”(对严重超载的名称感到抱歉)概念,这意味着您可以通过查看源代码看到当前的作用域和名称。

在过程编程中,如果您想回答这个问题,那么背后的价值是x什么?还需要知道如何到达那里,仅靠范围和名称是不够的。这就是我所面临的最大挑战,因为此执行路径是“运行时”属性,并且可能依赖于许多不同的事物,以至于大多数人都学会了调试它,而不尝试恢复执行路径。



0

两种编程范例之间的明显区别是状态。

在函数式编程中,避免状态。简而言之,将不会为变量分配任何值。

例:

def double(x):
    return x * 2

def doubleLst(lst):
    return list(map(double, action))

但是,过程编程使用状态。

例:

def doubleLst(lst):
    for i in range(len(lst)):
        lst[i] = lst[i] * 2  # assigning of value i.e. mutation of state
    return lst
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.