我真的很难理解过程和函数式编程范式之间的区别。
以下是 Wikipedia 关于函数式编程的条目的前两段:
在计算机科学中,函数式编程是一种编程范式,它将计算视为对数学函数的评估,并避免了状态和可变数据。它强调函数的应用,与强调状态变化的命令式编程风格相反。函数式编程起源于 lambda 演算,这是 1930 年代开发的一种形式系统,用于研究函数定义、函数应用和递归。许多函数式编程语言可以看作是对 lambda 演算的阐述。在实践中,数学函数与命令式编程中使用的“函数”概念之间的区别在于,命令式函数可能会产生副作用,从而改变程序状态的值。正因为如此,它们缺乏引用透明性,即相同的语言表达式可以在不同的时间根据执行程序的状态产生不同的值。相反,在函数式代码中,函数的输出值仅取决于输入到函数的参数,因此使用相同的参数 x 值调用函数 f 两次将产生相同的结果 f(x) 两次。消除副作用可以更容易理解和预测程序的行为,这是开发函数式编程的关键动机之一。
在第 2 段中说
相反,在函数式代码中,函数的输出值仅取决于输入到函数的参数,因此使用相同的参数 x 值调用函数 f 两次将产生相同的结果 f(x) 两次。
这不是过程编程的完全相同的情况吗?
在程序性与功能性方面应该寻找什么脱颖而出?
函数式编程
函数式编程是指将函数视为值的能力。
让我们考虑一个与“常规”值的类比。我们可以取两个整数值并使用 +
运算符将它们组合以获得新的整数。或者我们可以将一个整数乘以一个浮点数得到一个浮点数。
在函数式编程中,我们可以使用 compose 或 lift 等运算符组合两个函数值来生成一个新的函数值。或者,我们可以使用 map 或 fold 等运算符组合函数值和数据值以生成新的数据值。
请注意,许多语言都具有函数式编程能力——即使是通常不被认为是函数式语言的语言。甚至祖父 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));
}
从内到外,这个定义做了以下事情:
compose(odd, length) 结合了奇数和长度函数来产生一个新的函数来判断一个字符串的长度是否是奇数。 map(..., words) 为 words 中的每个元素调用该新函数,最终返回一个新的布尔值列表,每个值指示相应的单词是否具有奇数个字符。 apply(and, ...) 将“and”运算符应用于结果列表,并将所有布尔值组合在一起以产生最终结果。
从这些示例中可以看出,过程编程非常关注在变量中移动值并明确描述产生最终结果所需的操作。相比之下,函数式强调将初始输入转换为最终输出所需的函数组合。
该示例还显示了过程代码与功能代码的典型相对大小。此外,它表明过程代码的性能特征可能比功能代码更容易看到。考虑一下:这些函数是计算列表中所有单词的长度,还是在找到第一个偶数长度的单词后立即停止?另一方面,函数式代码允许高质量的实现执行一些相当严格的优化,因为它主要表达意图而不是显式算法。
延伸阅读
这个问题出现了很多......例如,请参阅:
过程式编程和函数式编程有什么区别?
有人可以给我函数式编程与命令式/过程式编程的例子吗?
OOP vs 函数式编程 vs 过程式
John Backus 的图灵奖演讲详细阐述了函数式编程的动机:
Can Programming Be Liberated from the von Neumann Style?
我真的不应该在目前的情况下提及那篇论文,因为它变得非常技术性,非常快。我无法抗拒,因为我认为这是真正的基础。
附录 - 2013
评论员指出,流行的当代语言提供了除过程和函数之外的其他编程风格。此类语言通常提供以下一种或多种编程风格:
查询(例如列表理解、语言集成查询)
数据流(例如隐式迭代、批量操作)
面向对象(例如封装的数据和方法)
面向语言的(例如特定于应用程序的语法、宏)
有关此响应中的伪代码示例如何从其他样式提供的一些工具中受益的示例,请参见下面的注释。特别是,程序示例将受益于几乎任何更高级别结构的应用。
展示的示例故意避免混合这些其他编程风格,以强调所讨论的两种风格之间的区别。
函数式编程和命令式编程之间的真正区别在于思维方式——命令式程序员正在考虑变量和内存块,而函数式程序员正在思考“我如何将输入数据转换为输出数据”——你的“程序”就是管道并对数据进行一组转换,以将其从输入带到输出。这是 IMO 有趣的部分,而不是“你不得使用变量”位。
由于这种心态,FP 程序通常描述会发生什么,而不是如何发生的具体机制 - 这很强大,因为如果我们能够清楚地说明“选择”、“在哪里”和“聚合”的含义,我们可以自由地交换它们的实现,就像我们使用 AsParallel() 所做的那样,突然我们的单线程应用程序扩展到 n 个内核。
Isn't that the same exact case for procedural programming?
不,因为程序代码可能有副作用。例如,它可以存储调用之间的状态。
也就是说,可以用被认为是程序性的语言编写满足此约束的代码。在某些被认为是功能性的语言中,也可以编写打破这种约束的代码。
我不同意 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)
如果您没有显式控制函数的全局依赖关系,这并不简单,或者甚至不可能。函数式编程的最佳特性是您可以始终如一地创建更通用的抽象并相信它们可以组合成一个更大的整体。
apply
与 fold
或 reduce
的操作并不完全相同,尽管我同意拥有非常通用的算法的良好能力。
apply
表示 fold
或 reduce
,但在我看来,它必须在这种情况下才能返回布尔值。
在过程范式中(我应该说“结构化编程”吗?),您共享可变内存和以某种顺序读/写它的指令(一个接一个)。
在函数范式中,您有变量和函数(在数学意义上:变量不会随时间变化,函数只能根据其输入进行计算)。
(这被过度简化了,例如,FPL 通常具有处理可变内存的工具,而过程语言通常可以支持高阶过程,因此事情并不那么明确;但这应该给你一个想法)
IBM Developerworks 中的 Charming Python: Functional programming in Python 确实帮助我理解了其中的区别。
特别是对于稍微了解 Python 的人来说,本文中的代码示例对比了函数式和程序式地做不同的事情,可以阐明程序式编程和函数式编程之间的区别。
在函数式编程中,为了推断符号(变量或函数名)的含义,您只需要知道两件事——当前范围和符号名称。如果您有一个具有不变性的纯函数式语言,那么这两个都是“静态的”(对于严重重载的名称)概念,这意味着您可以通过查看源代码来同时看到当前范围和名称。
在过程编程中,如果您想回答 x
背后的价值是什么问题,您还需要知道您是如何到达那里的,仅范围和名称是不够的。这是我认为最大的挑战,因为这个执行路径是一个“运行时”属性,并且可以依赖于许多不同的东西,大多数人学会了只是调试它而不是尝试恢复执行路径。
我最近一直在考虑 Expression Problem 方面的差异。 Phil Wadler's description 经常被引用,但对 this question 的公认答案可能更容易理解。基本上,命令式语言似乎倾向于选择一种方法来解决问题,而函数式语言倾向于选择另一种方法。
两种编程范式之间的一个明显区别是状态。
在函数式编程中,避免了状态。简而言之,不会有变量被赋值。
例子:
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
odd_words(words)
定义与答案的allOdd
不同。对于过滤和映射,通常首选列表推导式,但这里的函数allOdd
应该将单词列表简化为单个布尔值。