17 回答
函数式语言(理想情况下)允许您编写数学函数,即接受n 个参数并返回一个值的函数。如果程序被执行,这个函数会根据需要进行逻辑评估。1
另一方面,过程语言执行一系列顺序步骤。(有一种将顺序逻辑转换为函数逻辑的方法,称为连续传递样式。)
因此,纯函数式程序总是为输入产生相同的值,并且评估的顺序没有明确定义;这意味着像用户输入或随机值这样的不确定值很难用纯函数式语言建模。
1与此答案中的其他所有内容一样,这是一个概括。这个属性,在需要计算结果时而不是在调用它的地方按顺序评估计算,被称为“惰性”。并非所有函数式语言实际上都是普遍惰性的,惰性也不限于函数式编程。相反,这里给出的描述提供了一个“思维框架”来思考不同的编程风格,这些风格不是不同的和相反的类别,而是流动的想法。
基本上两种风格,就像阴阳。一个是有组织的,一个是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么至少有两种语言最近推出了一个新版本,包含两种编程风格。(Perl 6和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;
}
#功能:#
- 经常递归。
- 始终为给定输入返回相同的输出。
- 评估顺序通常是未定义的。
- 必须是无国籍的。ie 任何操作都不会产生副作用。
- 非常适合并行执行
- 倾向于强调分而治之的方法。
- 可能具有惰性评估的功能。
## Haskell ##(从维基百科复制);
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
我从未在其他地方看到过这个定义,但我认为这很好地总结了这里给出的差异:
函数式编程专注于表达式
过程式编程侧重于语句
表达式具有值。函数式程序是一个表达式,其值是计算机执行的一系列指令。
语句没有值,而是修改某些概念机器的状态。
在纯粹的函数式语言中,不会有语句,因为无法操纵状态(它们可能仍然有一个名为“语句”的句法结构,但除非它操纵状态,否则我不会在这个意义上称它为语句)。在纯粹的程序语言中,没有表达式,一切都是操纵机器状态的指令。
Haskell 将是纯函数式语言的一个示例,因为无法操作状态。机器代码将是纯过程语言的一个示例,因为程序中的所有内容都是操纵机器寄存器和内存状态的语句。
令人困惑的部分是绝大多数编程语言都包含表达式和语句,允许您混合范式。根据语言对语句和表达式的使用程度,可以将语言分为功能性更强或程序性更强。
例如,C 将比 COBOL 更具功能性,因为函数调用是一个表达式,而在 COBOL 中调用子程序是一个语句(它操纵共享变量的状态并且不返回值)。Python 将比 C 更实用,因为它允许您将条件逻辑表达为使用短路评估的表达式(测试 && path1 || path2 而不是 if 语句)。Scheme 会比 Python 更实用,因为 scheme 中的所有内容都是一个表达式。
您仍然可以使用鼓励程序范式的语言以功能样式编写,反之亦然。用这种语言不鼓励的范式写作会更难和/或更尴尬。
在计算机科学中,函数式编程是一种编程范式,它将计算视为对数学函数的评估,并避免了状态和可变数据。它强调函数的应用,与强调状态变化的过程式编程风格形成对比。
功能编程
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。
免责声明:显然这是对现实的超简化视图。这个答案只是给人一种“功能”的感觉,而不是“程序”。而已。一旦你尝到了这种肤浅却深入人心的直觉,就开始探索这两种范式,你就会开始很清楚地看到区别。
帮助我的学生,希望对你也有帮助。
我相信过程/功能/目标编程是关于如何解决问题的。
第一种风格将所有事情都计划到步骤中,并通过一次执行一个步骤(一个过程)来解决问题。另一方面,函数式编程将强调分而治之的方法,将问题划分为子问题,然后解决每个子问题(创建一个函数来解决该子问题)并将结果组合为为整个问题创造答案。最后,目标编程将通过在计算机内部创建一个包含许多对象的迷你世界来模仿现实世界,每个对象都有(有些)独特的特征,并与其他对象交互。从这些互动中会出现结果。
每种编程风格都有自己的优点和缺点。因此,做诸如“纯编程”之类的事情(即纯程序化——顺便说一句,没有人这样做,这有点奇怪——或者纯功能性或纯客观性)是非常困难的,如果不是不可能的话,除了一些特别的基本问题旨在展示编程风格的优势(因此,我们称那些喜欢纯粹的人为“weenie”:D)。
然后,从这些风格中,我们可以设计出针对每种风格进行优化的编程语言。例如,Assembly 是关于程序的。好吧,大多数早期的语言都是过程性的,不仅是 Asm,比如 C、Pascal (我听说还有 Fortran)。然后,我们有所有著名的 Java 在客观学校(实际上,Java 和 C# 也在一个名为“面向金钱”的类中,但这是另一个讨论的主题)。Smalltalk也是客观的。在函数式学校,我们会有“几乎函数式”(有些人认为它们是不纯的)Lisp 家族和 ML 家族以及许多“纯粹函数式”的 Haskell、Erlang 等。顺便说一下,还有许多通用语言,例如 Perl、Python ,鲁比。
扩展康拉德的评论:
因此,纯函数式程序总是为输入产生相同的值,并且评估的顺序没有明确定义;
因此,函数式代码通常更容易并行化。由于函数(通常)没有副作用,并且它们(通常)只是根据它们的参数采取行动,因此很多并发问题都消失了。
当您需要能够证明您的代码是正确的时,也可以使用函数式编程。这对于过程式编程要困难得多(函数式编程不容易,但仍然更容易)。
免责声明:我已经很多年没有使用函数式编程了,直到最近才重新开始研究它,所以我在这里可能并不完全正确。:)
我没有看到这里真正强调的一件事是,像 Haskell 这样的现代函数式语言实际上更多的是用于流控制的一流函数,而不是显式递归。您不需要像上面所做的那样在 Haskell 中递归地定义阶乘。我想像
fac n = foldr (*) 1 [1..n]
是一个完美的惯用结构,在精神上更接近于使用循环而不是使用显式递归。
函数式编程与不使用全局变量的过程式编程相同。
过程语言倾向于跟踪状态(使用变量)并倾向于作为一系列步骤执行。纯函数式语言不跟踪状态,使用不可变值,并且倾向于作为一系列依赖项执行。在许多情况下,调用堆栈的状态将保存与存储在过程代码中的状态变量中的信息等效的信息。
递归是函数式编程的经典示例。
康拉德说:
因此,纯函数式程序总是为输入产生相同的值,并且评估的顺序没有明确定义;这意味着像用户输入或随机值这样的不确定值很难用纯函数式语言建模。
纯函数式程序中的评估顺序可能很难(呃)推理(尤其是懒惰)甚至不重要,但我认为说它没有很好定义会让听起来你无法判断你的程序是否正在运行工作!
也许更好的解释是函数式程序中的控制流是基于何时需要函数参数的值。关于这一点的好处是,在编写良好的程序中,状态变得明确:每个函数将其输入作为参数列出,而不是任意修改全局状态。所以在某种程度上,更容易推断一次关于一个函数的评估顺序。每个功能都可以忽略宇宙的其余部分并专注于它需要做的事情。组合时,函数可以保证与它们在隔离时一样[1] 工作。
... 用户输入或随机值等不确定值很难用纯函数式语言建模。
纯函数式程序中输入问题的解决方案是使用足够强大的抽象将命令式语言嵌入为DSL。在命令式(或非纯函数式)语言中,这不是必需的,因为您可以“作弊”并隐式传递状态,并且评估顺序是明确的(无论您喜欢与否)。由于这种“作弊”和对每个函数的所有参数的强制评估,在命令式语言中 1)您失去了创建自己的控制流机制(没有宏)的能力,2)代码本质上不是线程安全和/或可并行化的默认情况下, 3) 并实现类似 undo(时间旅行)之类的东西需要仔细的工作(命令式程序员必须存储一个恢复旧值的方法!),而纯函数式编程可以为您购买所有这些东西——我可能还会更多忘记了——“免费”。
我希望这听起来不像狂热,我只是想添加一些观点。命令式编程,尤其是使用 C# 3.0 等强大语言的混合范式编程仍然是完成工作的完全有效方法,并且没有灵丹妙药。
[1] ...除非可能与内存使用有关(参见 Haskell 中的 foldl 和 foldl')。
扩展康拉德的评论:
并且评估的顺序没有明确定义
一些函数式语言有所谓的惰性求值。这意味着在需要该值之前不会执行函数。在那之前,函数本身就是被传递的东西。
程序语言是第 1 步第 2 步第 3 步……如果在第 2 步中您说加 2 + 2,那么它会正确执行。在惰性求值中,您会说加 2 + 2,但如果从未使用过结果,则它永远不会进行加法。
如果你有机会,我会推荐一份 Lisp/Scheme 的副本,并在其中做一些项目。最近成为潮流的大多数想法在几十年前都在 Lisp 中表达过:函数式编程、延续(作为闭包)、垃圾收集,甚至是 XML。
因此,这将是在所有这些当前想法以及其他一些想法(如符号计算)上抢占先机的好方法。
你应该知道函数式编程有什么好处,什么不好。这对一切都不好。有些问题最好用副作用来表达,即同一个问题会根据被问到的时间给出不同的答案。
@克赖顿:
在 Haskell 中有一个名为product的库函数:
prouduct list = foldr 1 (*) list
或者简单地说:
product = foldr 1 (*)
所以“惯用的”阶乘
fac n = foldr 1 (*) [1..n]
简直就是
fac n = product [1..n]
过程编程将语句序列和条件构造划分为单独的块,这些块称为过程,这些过程在作为(非功能性)值的参数上进行参数化。
函数式编程是相同的,只是函数是一等值,因此它们可以作为参数传递给其他函数并作为函数调用的结果返回。
请注意,在这种解释中,函数式编程是过程式编程的概括。然而,少数人将“函数式编程”解释为无副作用,这与除 Haskell 之外的所有主要函数式语言完全不同但无关紧要。
这里没有一个答案显示惯用的函数式编程。递归阶乘答案非常适合表示 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这样的新语言内置的。函数式编程的繁重工作是通过函数/概念完成的,例如map
, filter
, reduce
, currying
, partial
,您可以查看其中的最后三个以进一步理解。
附录
为了在野外使用,编译器通常必须弄清楚如何在内部将函数版本转换为过程版本,因为函数调用开销太高。递归情况(例如所示的阶乘)将使用诸如尾调用之类的技巧来消除 O(n) 内存使用量。没有副作用的事实使函数式编译器&& ret
即使在.reduce
最后完成时也可以实现优化。在 JS 中使用 Lodash,显然不允许进行任何优化,因此会影响性能(这通常不是 Web 开发关注的问题)。像 Rust 这样的语言会在内部进行优化(并具有try_fold
辅助&& ret
优化等功能)。
要了解差异,需要了解过程和函数式编程的“教父”范式是命令式编程。
基本上,过程式编程只是一种构造命令式程序的方式,其中主要的抽象方法是“过程”。(或某些编程语言中的“函数”)。甚至面向对象编程也只是构造命令式程序的另一种方式,其中状态被封装在对象中,成为具有“当前状态”的对象,而且该对象具有一组函数、方法和其他东西,可以让你程序员操作或更新状态。
现在,关于函数式编程,其方法的要点是它确定要采用什么值以及应该如何传递这些值。(因此没有状态,也没有可变数据,因为它将函数作为第一类值并将它们作为参数传递给其他函数)。
PS:理解每种编程范式的用途应该澄清它们之间的差异。
PSS:归根结底,编程范式只是解决问题的不同方法。
PSS:这个quora 答案有一个很好的解释。