我已经阅读了两者的维基百科文章 过程式编程函数式编程, ,但我还是有点困惑。有人能把它归结为核心吗?

有帮助吗?

解决方案

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

另一方面,过程语言执行一系列 顺序的 脚步。(有一种将顺序逻辑转换为函数逻辑的方法,称为 连续传球风格.)

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


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

其他提示

基本上有两种风格,就像阴和阳。一种是有组织的,另一种是混乱的。在某些情况下,函数式编程是显而易见的选择,而在其他情况下,过程式编程是更好的选择。这就是为什么至少有两种语言最近推出了包含两种编程风格的新版本。 ( Perl 6D 2 )

程序:

  • 例程的输出并不总是与输入直接相关。
  • 一切都按特定顺序完成。
  • 例程的执行可能会产生副作用。
  • 倾向于强调以线性方式实施解决方案。

珀尔6

d 2

int factorial( int n ){

  int result = 1;

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

  return result;
}

功能性:

  • 经常递归。
  • 对于给定的输入始终返回相同的输出。
  • 评估顺序通常是不确定的。
  • 一定是无国籍的。IE。任何手术都不可能有副作用。
  • 非常适合并行执行
  • 倾向于强调分而治之的方法。
  • 可能具有惰性求值的特征。

哈斯克尔

(复制自 维基百科 );

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

珀尔6

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 实现中的大多数运算符都是这样定义的。它还允许您将自己的多个候选者添加到现有运算符中。

此示例还显示了范围创建(2..$n) 和列表缩减元运算符 ([ OPERATOR ] LIST) 与数字中缀乘法运算符结合使用。(*)
它还表明您可以将 --> UInt 在签名中而不是 returns UInt 在它之后。

(您可以通过以下方式开始范围 2 因为乘法“运算符”将返回 1 当不带任何参数调用时)

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

功能性 编程的重点是 表达式

程序 编程的重点是 声明

表达式具有值。函数式程序是一种表达式,其值是计算机要执行的指令序列。

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

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

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

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

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

您仍然可以使用鼓励过程范式的语言以函数式风格进行编写,反之亦然。只是用语言不鼓励的范式编写会更困难和/或更尴尬。

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

我相信过程/函数/目标编程是关于如何解决问题的。

第一种风格将所有事情分成步骤进行规划,并通过一次实施一个步骤(一个过程)来解决问题。另一方面,函数式编程强调分而治之的方法,将问题分为子问题,然后解决每个子问题(创建一个函数来解决该子问题),并将结果组合起来创建整个问题的答案。最后,客观编程将通过在计算机内部创建一个包含许多对象的迷你世界来模仿现实世界,每个对象都有(某种程度上)独特的特征,并与其他对象交互。从这些互动中,结果就会显现出来。

每种编程风格都有其自身的优点和缺点。因此,做一些诸如“纯编程”之类的事情(即纯粹的程序性——顺便说一句,没有人这样做,这有点奇怪——或者纯粹的功能性或纯粹的客观性)是非常困难的,如果不是不可能的话,除了一些专门设计来展示编程风格的优点的基本问题(因此,我们称那些喜欢纯洁的人为“weenie”:D)。

然后,根据这些风格,我们设计了针对每种风格进行优化的编程语言。例如,汇编就是程序性的。好吧,大多数早期语言都是过程语言,不仅是 Asm,还有 C、Pascal(我听说还有 Fortran)。然后,我们所有著名的Java都在客观学派中(实际上,Java和C#也属于一个名为“金钱导向”的类别,但这是另一个讨论的主题)。Smalltalk也是一个目标。在函数式学派中,我们会有“近函数式”(有些人认为它们是不纯的)Lisp 系列和 ML 系列以及许多“纯函数式”Haskell、Erlang 等。顺便说一下,通用语言有很多,比如Perl、Python、Ruby。

扩展康拉德的评论:

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

因此,函数代码通常更容易并行化。由于函数(通常)没有副作用,并且它们(通常)只是根据其参数进行操作,因此许多并发问题就消失了。

当您需要具备以下能力时,也可以使用函数式编程 证明 你的代码是正确的。这对于过程编程来说要困难得多(对于函数式编程来说不容易,但仍然更容易)。

免责声明:我已经很多年没有使用函数式编程了,直到最近才开始重新审视它,所以我在这里可能不完全正确。:)

我在这里没有看到真正强调的一件事是,现代函数式语言(例如 Haskell)实际上更多地使用一流函数来进行流控制,而不是显式递归。您不需要像上面那样在 Haskell 中递归地定义阶乘。我认为像

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

是一种完全惯用的结构,在精神上更接近于使用循环而不是使用显式递归。

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

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

递归是函数式编程的经典示例。

康拉德说:

结果,纯粹的功能程序总是为输入产生相同的值,并且评估顺序没有明确定义。这意味着不确定的值(例如用户输入或随机值)很难以纯粹的功能性语言进行建模。

纯函数式程序中的求值顺序可能很难(呃)推理(尤其是因为懒惰),甚至不重要,但我认为说它没有很好地定义会让听起来你无法判断你的程序是否正在运行根本不去工作!

也许更好的解释是函数式程序中的控制流基于何时需要函数参数的值。这样做的好处是,在编写良好的程序中,状态变得明确:每个函数都将其输入列为参数,而不是任意列出 咀嚼 全局状态。所以在某种程度上, 更容易推断一次针对一个函数的求值顺序. 。每个功能都可以忽略宇宙的其余部分并专注于它需要做的事情。组合后,函数保证与单独运行时的工作方式相同[1]。

...诸如用户输入或随机值之类的不确定值很难以纯粹的功能语言进行建模。

纯函数式程序中输入问题的解决方案是嵌入命令式语言作为 DSL 使用 足够强大的抽象. 。在命令式(或非纯函数式)语言中,这是不需要的,因为您可以“作弊”并隐式传递状态,并且评估顺序是明确的(无论您是否喜欢)。由于这种“作弊”和对每个函数的所有参数的强制评估,在命令式语言中 1)您失去了创建自己的控制流机制(没有宏)的能力,2)代码本质上不是线程安全的和/或可并行的 默认情况下, ,3)并实现诸如撤消(时间旅行)之类的东西需要仔细的工作(命令式程序员必须存储恢复旧值的方法!),而纯函数式编程可以为您购买所有这些东西 - 以及更多我可能会做的事情忘记了——“免费”。

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

[1] ...除非可能涉及内存使用(参见Haskell 中的 Foldl 和 Foldl')。

扩展康拉德的评论:

评估顺序没有明确定义

一些函数式语言具有所谓的“惰性求值”。这意味着函数在需要该值之前不会执行。在那之前,函数本身就是被传递的。

过程语言是步骤 1 步骤 2 步骤 3...如果在第 2 步中你说加 2 + 2,那么它就会正确执行。在惰性计算中,您会说加 2 + 2,但如果从未使用结果,则它永远不会执行加法。

如果你有机会,我建议你获取一份 Lisp/Scheme 的副本,并在其中做一些项目。最近流行的大多数想法都是在几十年前用 Lisp 表达的:函数式编程、延续(如闭包)、垃圾收集,甚至 XML。

因此,这将是一个很好的方法,可以让我们在所有这些当前的想法以及其他一些想法(例如符号计算)方面取得先机。

您应该知道函数式编程有什么好处,有什么坏处。这并不对一切都有好处。有些问题最好用副作用来表达,即同一问题根据提出的时间不同而给出不同的答案。

@克赖顿:

Haskell中有一个库函数叫做 产品:

prouduct list = foldr 1 (*) list

或者简单地:

product = foldr 1 (*)

所以“惯用的”阶乘

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

简单地说就是

fac n = product [1..n]

函数式编程

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.

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

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

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

要理解其中的区别,我们需要理解过程式编程和函数式编程的“教父”范式是 命令式编程.

基本上,程序编程只是构建命令式程序的一种方式,其中主要抽象方法是“程序”。 (或某些编程语言中的“功能”)。即使面向对象编程也只是构造命令式程序的另一种方式,其中状态被封装在对象中,成为具有“当前状态”的对象,并且该对象具有一组函数、方法和其他东西,可以让您程序员操纵或更新状态。

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

附:了解每种编程范例的用途应该能够澄清它们之间的差异。

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

附: quora 答案有很好的解释。

这里的答案都没有显示惯用的函数式编程。递归阶乘答案非常适合表示 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

虽然纯函数语言通常是研究语言(因为现实世界喜欢免费的副作用),但现实世界的过程语言将在适当的时候使用更简单的函数语法。

这通常是通过外部库来实现的,例如 洛达什, ,或者可以使用较新的语言内置,例如 . 。函数式编程的繁重工作是通过函数/概念完成的,例如 map, filter, reduce, currying, partial, ,您可以查找其中的后三个以进一步了解。

附录

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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top