过程式编程和函数式编程有啥区别? [关闭]
Posted
技术标签:
【中文标题】过程式编程和函数式编程有啥区别? [关闭]【英文标题】:What is the difference between procedural programming and functional programming? [closed]过程式编程和函数式编程有什么区别? [关闭] 【发布时间】:2010-09-06 14:25:21 【问题描述】:我已经阅读了procedural programming 和functional programming 的***文章,但我仍然有些困惑。有人能把它归结为核心吗?
【问题讨论】:
***暗示 FP 是(即始终是)声明式编程的子集,但 is not true and conflates the taxonomy of IP vs. DP. 【参考方案1】:函数式语言(理想情况下)允许您编写数学函数,即接受 n 个参数并返回值的函数。如果程序被执行,这个函数会根据需要进行逻辑评估。1
另一方面,过程语言执行一系列顺序步骤。 (有一种将时序逻辑转换为函数逻辑的方法,称为continuation passing style。)
因此,纯函数式程序总是为输入产生相同的值,并且评估顺序没有明确定义;这意味着用户输入或随机值等不确定值很难用纯函数式语言建模。
1 与此答案中的其他所有内容一样,这是一个概括。这个属性,在需要计算结果时而不是在调用它的地方按顺序评估计算,被称为“惰性”。并非所有函数式语言实际上都是普遍惰性的,惰性也不限于函数式编程。相反,这里给出的描述提供了一个“思维框架”来思考不同的编程风格,这些风格不是截然不同的相反类别,而是流动的想法。
【讨论】:
用户输入或随机值等不确定值很难用纯函数式语言建模,但这是一个已解决的问题。见单子。 “顺序步骤,函数程序将被嵌套”意味着通过emphasizing function composition提供关注点分离,即分离确定性子计算之间的依赖关系计算。 这似乎不对——过程也可以嵌套,过程可以有参数 @Hurda 是的,本可以更好地表述这一点。关键是过程式编程以预先确定的顺序逐步发生,而函数式程序不是逐步执行的;而是在需要时计算值。然而,缺乏对编程术语的普遍认可的定义使得这种概括几乎毫无用处。我已经在这方面修改了我的答案。【参考方案2】:基本上这两种风格,就像阴阳。一个是有组织的,一个是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么至少有两种语言最近推出了一个新版本,包含两种编程风格。 (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;
#Functional:#
通常是递归的。 对于给定的输入总是返回相同的输出。 评估顺序通常是未定义的。 必须是无国籍的。即任何操作都不会产生副作用。 非常适合并行执行 倾向于强调分而治之的方法。 可能具有惰性求值功能。##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
)
【讨论】:
嗨,考虑到 Perl 6 中的阶乘实现示例,您能否为“程序”中提到的以下 2 点提供一个示例。1)例程的输出并不总是具有直接相关性与输入。 2) 执行例程可能会产生副作用。sub postfix:<!> ($n) [*] 1..$n
@BradGilbert No operation can have side effects
-你能详细说明一下吗?
可能是我能找到的最好的答案......而且,我对这些个别观点做了一些研究......这真的帮助了我! :)
@AkashBisariya sub foo( $a, $b ) ($a,$b).pick
← 并不总是为相同的输入返回相同的输出,而以下是 sub foo( $a, $b ) $a + $b
【参考方案3】:
我从未在其他地方看到过这个定义,但我认为这很好地总结了这里给出的差异:
函数式编程侧重于表达式
程序化编程侧重于语句
表达式具有值。函数式程序是一个表达式,其值是计算机执行的一系列指令。
语句没有值,而是修改一些概念机器的状态。
在纯粹的函数式语言中,没有语句,因为无法操纵状态(它们可能仍然有一个名为“语句”的句法结构,但除非它操纵状态,否则我不会称它为语句在这个意义上说)。在纯过程语言中,没有表达式,一切都是操纵机器状态的指令。
Haskell 将是纯函数式语言的一个例子,因为没有办法操纵状态。机器代码将是纯过程语言的一个示例,因为程序中的所有内容都是操作机器寄存器和内存状态的语句。
令人困惑的部分是,绝大多数编程语言都包含表达式和语句,允许您混合范式。根据语言对语句和表达式的使用程度,可以将语言分为功能性更强或程序性更强。
例如,C 将比 COBOL 更实用,因为函数调用是一个表达式,而在 COBOL 中调用子程序是一个语句(操纵共享变量的状态并且不返回值)。 Python 将比 C 更实用,因为它允许您将条件逻辑表达为使用短路评估的表达式(测试 && path1 || path2 而不是 if 语句)。 Scheme 将比 Python 更实用,因为 scheme 中的所有内容都是一个表达式。
您仍然可以使用鼓励程序范式的语言以函数式风格进行编写,反之亦然。用这种语言不鼓励的范式写作会更难和/或更尴尬。
【讨论】:
我在网上看到的最好最简洁的解释,太棒了! C 也有布尔表达式 很好的解释。评论:“Python 将比 C 更实用,因为它允许您将条件逻辑表达为使用短路评估的表达式(测试 && path1 || path2 而不是 if 语句)” 在 C 中,您可以也做test && path1 || path2
。 “语句没有值,而是修改一些概念机器的状态。” 很难想出一个例子,除非我误解了这个定义。例如return
我认为是一个语句,但它本身并不会修改状态。
@Nagev a = sum(2, 3); sum
中的 return
正在修改变量 a
对吗?所以基本上状态变化?【参考方案4】:
在计算机科学中,函数式编程是一种编程范式,它将计算视为对数学函数的评估,并避免使用状态和可变数据。它强调函数的应用,与强调状态变化的过程式编程风格形成鲜明对比。
【讨论】:
虽然这是对我帮助最大的解释,但我对函数式编程的概念仍然很模糊。我正在寻找一种不依赖于引用外部对象来运行的编程风格(函数需要运行的每一件事都应该作为参数传入)。例如,我永远不会将GetUserContext()
放在函数中,会传入用户上下文。这是函数式编程吗?提前致谢。【参考方案5】:
函数式编程
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。
免责声明:显然这是对现实的超简化视图。这个答案只是给人一种“功能”的感觉,而不是“程序”。而已。一旦你尝到了这种肤浅却深入人心的直觉,就开始探索这两种范式,你就会开始看得很清楚了。
帮助我的学生,希望对你也有帮助。
【讨论】:
这个例子很容易理解函数式编程中“无状态”和“不可变数据”的术语,通读上面列出的所有定义和差异在阅读这个答案之前并没有消除我的困惑。谢谢! 可能有点挑剔,但函数示例在函数内部仍然包含一个可变变量,并且函数调用以特定顺序作为语句执行。这两个属性通常不被认为是功能性的。 "即使你运行该函数五次,每次它都会返回 2" 除非,num=fx_add_one(num) 这是你会做的,除非你传递一个指针,在这种情况下你会'不需要返回值来更新 num。您显示 fx_add_one(num) 的方式只是 func 的最基本用途,而且很少使用,而且您的 func 返回一个值,您特别忽略它来尝试并说明您的观点几乎不存在,因为您不会像这样编码首先。您还可以使用 func 创建程序示例。没有理由不能访问 func 中的全局变量并将其称为 5x。【参考方案6】:我相信过程/功能/目标编程是关于如何解决问题的。
第一种风格将所有事情都计划到步骤中,并通过一次执行一个步骤(一个过程)来解决问题。另一方面,函数式编程将强调分而治之的方法,将问题划分为子问题,然后解决每个子问题(创建一个函数来解决该子问题)并将结果组合为为整个问题创造答案。最后,目标编程将通过在计算机内部创建一个包含许多对象的迷你世界来模仿现实世界,每个对象都有(有些)独特的特征,并与其他对象交互。从这些互动中会产生结果。
每种编程风格都有自己的优点和缺点。因此,做诸如“纯编程”之类的事情(即纯程序化 - 顺便说一句,没有人这样做,这有点奇怪 - 或纯功能性或纯客观性)是非常困难的,如果不是不可能的话,除了一些特别的基本问题旨在展示编程风格的优势(因此,我们称那些喜欢纯粹的人为“weenie”:D)。
然后,从这些样式中,我们可以设计出针对每种样式进行优化的编程语言。例如,Assembly 是关于程序的。好吧,大多数早期的语言都是过程性的,不仅是 Asm,比如 C、Pascal (我听说还有 Fortran)。然后,我们有所有著名的 Java 客观学校(实际上,Java 和 C# 也属于“面向金钱”的一类,但这是另一个讨论的主题)。 Smalltalk也是客观的。在函数式学校,我们会有“几乎函数式”(有些人认为它们是不纯的)Lisp 家族和 ML 家族以及许多“纯粹函数式”的 Haskell、Erlang 等。顺便说一下,有许多通用语言,如 Perl、Python ,鲁比。
【讨论】:
【参考方案7】:扩展康拉德的评论:
因此,纯函数式程序总是为输入产生相同的值,并且评估的顺序没有明确定义;
因此,函数式代码通常更容易并行化。由于函数(通常)没有副作用,而且它们(通常)只是根据自己的参数进行操作,因此很多并发问题都消失了。
当您需要能够证明您的代码是正确的时,也可以使用函数式编程。这对于过程式编程要困难得多(函数式编程不容易,但仍然更容易)。
免责声明:我已经很多年没有使用函数式编程了,最近才重新开始研究它,所以我在这里可能并不完全正确。 :)
【讨论】:
【参考方案8】:我没有看到这里真正强调的一件事是,像 Haskell 这样的现代函数式语言实际上更多地关注用于流控制的一流函数,而不是显式递归。您不需要像上面所做的那样在 Haskell 中递归地定义阶乘。我觉得像
fac n = foldr (*) 1 [1..n]
是一个完美的惯用结构,在精神上更接近于使用循环而不是使用显式递归。
【讨论】:
【参考方案9】:函数式编程与不使用全局变量的过程式编程相同。
【讨论】:
【参考方案10】:过程语言倾向于跟踪状态(使用变量)并倾向于作为一系列步骤执行。纯函数式语言不跟踪状态,使用不可变值,并且倾向于作为一系列依赖项执行。在许多情况下,调用堆栈的状态将保存与存储在过程代码中的状态变量中的信息等效的信息。
递归是函数式编程的经典例子。
【讨论】:
读完这个页面后,我想到了同样的事情->“递归是函数式编程的经典例子”,你清除了它。谢谢,现在我想我得到了一些东西。跨度> 【参考方案11】:康拉德说:
因此,纯函数式程序总是为输入产生相同的值, 并且评估顺序没有明确定义;这意味着不确定的值像 用户输入或随机值很难用纯函数式语言建模。
纯函数式程序中的评估顺序可能很难(呃)推理(尤其是懒惰),甚至不重要,但我认为说它没有明确定义会让听起来你无法判断你的程序完全可以运行!
也许更好的解释是函数式程序中的控制流是基于何时需要函数参数的值。关于这一点的好处是,在编写良好的程序中,状态变得明确:每个函数将其输入列为参数,而不是任意的munging 全局状态。因此,在某种程度上,更容易推断一次针对一个函数的评估顺序。每个功能都可以忽略宇宙的其余部分并专注于它需要做的事情。组合时,函数可以保证与它们单独工作时一样[1]。
... 用户输入或随机值等不确定值很难纯粹建模 函数式语言。
纯函数式程序中输入问题的解决方案是使用a sufficiently powerful abstraction 将命令式语言嵌入为DSL。在命令式(或非纯函数式)语言中,这不是必需的,因为您可以“作弊”并隐式传递状态,并且评估顺序是明确的(无论您喜欢与否)。由于这种“欺骗”和对每个函数的所有参数的强制评估,在命令式语言中 1)您失去了创建自己的控制流机制(没有宏)的能力,2)代码本质上不是线程安全和/或可并行化的默认情况下, 3) 并实现类似 undo(时间旅行)之类的东西需要仔细的工作(命令式程序员必须存储恢复旧值的秘诀!),而纯函数式编程会为你买单这些东西——还有一些我可能已经忘记了——“免费”。
我希望这听起来不像是***,我只是想补充一些观点。命令式编程,尤其是使用 C# 3.0 等强大语言的混合范式编程仍然是完成工作的完全有效方法,there is no silver bullet。
[1] ... 可能与内存使用有关(参见 Haskell 中的 foldl 和 foldl')。
【讨论】:
【参考方案12】:扩展康拉德的评论:
并且评估的顺序不是 明确的
一些函数式语言有所谓的惰性求值。这意味着在需要该值之前不会执行函数。在那之前,函数本身就是被传递的。
程序语言是第 1 步第 2 步第 3 步...如果在第 2 步中您说加 2 + 2,那么它会正确执行。在惰性求值中,你会说相加 2 + 2,但如果从未使用过结果,则永远不会进行相加。
【讨论】:
【参考方案13】:如果你有机会,我建议你获取一份 Lisp/Scheme 的副本,并在其中做一些项目。最近成为潮流的大多数想法都是在几十年前用 Lisp 表达的:函数式编程、延续(作为闭包)、垃圾收集,甚至是 XML。
因此,这是在所有这些当前想法以及其他一些想法(例如符号计算)上抢占先机的好方法。
您应该知道函数式编程的好处和坏处。这对一切都不好。有些问题最好用副作用来表达,即同一个问题会根据被问到的时间给出不同的答案。
【讨论】:
【参考方案14】:@Creighton:
在 Haskell 中有一个名为 product 的库函数:
prouduct list = foldr 1 (*) list
或者简单地说:
product = foldr 1 (*)
所以“惯用的”阶乘
fac n = foldr 1 (*) [1..n]
应该是
fac n = product [1..n]
【讨论】:
这没有提供问题的答案。要批评或要求作者澄清,请在其帖子下方发表评论。 我相信这是多年前的帖子,在评论系统还没有添加之前,如果你能相信的话:***.com/help/badges/30/beta?userid=2543【参考方案15】:过程式编程将语句序列和条件构造划分为单独的块,称为过程,这些过程通过(非函数式)值的参数进行参数化。
函数式编程是相同的,只是函数是一等值,因此它们可以作为参数传递给其他函数并作为函数调用的结果返回。
请注意,在这种解释中,函数式编程是过程式编程的概括。然而,少数人将“函数式编程”解释为无副作用,这与除 Haskell 之外的所有主要函数式语言完全不同但无关紧要。
【讨论】:
【参考方案16】:这里的答案都没有显示惯用的函数式编程。递归阶乘答案非常适合在 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
等函数/概念完成的,您可以查看后三个以进一步了解。
附录
为了在野外使用,编译器通常必须弄清楚如何在内部将函数版本转换为过程版本,因为函数调用开销太高。递归情况(例如所示的阶乘)将使用诸如tail call 之类的技巧来消除 O(n) 内存使用量。即使 .reduce
最后完成,没有副作用的事实允许函数式编译器实现 && ret
优化。在 JS 中使用 Lodash,显然不允许进行任何优化,因此会影响性能(这通常不是 Web 开发的问题)。像 Rust 这样的语言会在内部进行优化(并且有 try_fold
等函数来辅助 && ret
优化)。
【讨论】:
【参考方案17】:要了解区别,需要了解过程和函数式编程的“教父”范式是命令式编程。
基本上,过程式编程只是一种构建命令式程序的方式,其中主要的抽象方法是“过程”。 (或某些编程语言中的“函数”)。甚至面向对象编程也只是构造命令式程序的另一种方式,其中状态被封装在对象中,成为具有“当前状态”的对象,而且该对象具有一组函数、方法和其他东西,可以让你程序员操作或更新状态。
现在,关于函数式编程,其方法中的要点 是确定要采用哪些值以及应该如何传输这些值。 (所以没有状态,也没有可变数据,因为它将函数作为第一类值并将它们作为参数传递给其他函数)。
PS:了解每种编程范式的用途应该会澄清它们之间的差异。
PSS:归根结底,编程范式只是解决问题的不同方法。
PSS:this quora 回答有很好的解释。
【讨论】:
以上是关于过程式编程和函数式编程有啥区别? [关闭]的主要内容,如果未能解决你的问题,请参考以下文章