"函数"和"过程"有什么区别?

261

一般来说,在编程语言中,我们都会听到 函数过程 的概念。然而,我刚刚发现我几乎将这些术语互换使用(可能是非常错误的)。

那么我的问题是:

就它们的功能、目的和用法而言,它们有什么区别?请举个例子。

如果能提供一个例子就更好了。


参见:https://dev59.com/xmkv5IYBdhLWcg3wvDNF - gerrit
6
我认为SICP(《计算机程序的构造和解释》)说得很对。函数只存在于数学中,它们代表了“是什么”知识。程序存在于编程语言(包括函数式语言)中,它们代表了“如何做”的知识。例如: 函数:sqrt(x) = y,其中y满足y^2=x。程序(define (sqrt x) (newtons-method (lambda (y) (- (square y) x)) 1.0)) - mk12
3
我猜SICP是计算机程序的构造和解释这本书的缩写。 - jmrah
18个回答

366

一个函数会返回一个值,而一个过程则只是执行一系列命令。

函数这个名字来源于数学。它用于根据输入计算一个值。

过程是一组按顺序执行的命令。

在大多数编程语言中,即使是函数也可以有一组命令。因此,区别只在于是否返回值。

但是如果你想保持函数的简洁性(只看看函数式语言),那么你需要确保函数没有副作用。


你如何确保在命令式语言(Java,C)或声明式语言(Scala,Scheme)中没有副作用? - orlybg
2
@orlybg,在声明式语言中,一致性来自于语言的实现。它们的作用域限制防止了它们产生副作用。另一方面,命令式语言明确地利用它们的副作用。副作用并不总是坏的。 - Tharindu Rusira
3
在Pascal中,过程没有返回语句,只有函数才有。这段文字可能存在错误。然而,一个过程可以有一个"exit"语句,它可以充当一个没有参数的"return"语句,意味着没有返回值。 - Eric Fortier
函数可以获取输入并仅返回一个输出。过程或宏可以获取输入但不返回任何数据,只执行一定数量的语句。主要区别在于过程无法返回任何数据类型。 - EsmaeelE
但实际上有点令人困惑的是,例如在Ada中,您可以将过程参数明确声明为“in”、“out”和“in out”。因此,“out”过程参数并不完全相同于返回语句,只是像副作用一样接收返回值(因此,您可以调用plusOne(2, i)而不是int i = plusOne(2);)。 - Vegaaaa
显示剩余7条评论

52

这取决于上下文。

在类Pascal语言中,函数和过程是不同的实体,它们之间的区别在于它们是否返回值。它们在语言语法方面有所不同(例如,过程调用形成语句;您无法在表达式内部使用过程调用,而函数调用不形成语句,您必须在其他语句中使用它们)。因此,受Pascal影响的程序员会对它们进行区分。

在类C语言和许多其他现代语言中,这种区别已经消失了;在静态类型的语言中,过程只是带有奇怪返回类型的函数。这可能就是为什么它们可以互换使用。

在函数式编程语言中,通常没有过程这样的概念-一切都是函数。


2
编程语言的文档可以随意命名函数和过程,因为人们会接受任何名称,因为这些名称背后的背景早已被淡化。 - Arne Babenhauserheide

27

C语言示例:

// function
int square( int n ) {
   return n * n;
}

// procedure
void display( int n ) {
   printf( "The value is %d", n );
}

虽然您应该注意到,C标准只涉及函数而不是过程。


6
C标准只讨论函数,不涉及过程。这是因为它只有函数。返回空值的函数称为“void函数”。Kernighan&Ritchie在第1.7章中指出:“在C中,函数等同于Fortran中的子程序或函数,或者Pascal中的过程或函数。”换句话说,这个答案是错误的。 - Mogsdad
13
答案没有错,而且很好地展示了纯函数和过程之间的区别。K&R把所有的子程序都称为“函数”以保持简单,但是带有副作用的子程序实际上是“过程”,而不是数学上的“函数”。如果C语言能够区分真正的函数和过程,那么它可能会更好,这将有助于静态分析、性能优化和并行化。 - Sam Watkins

13

一般来说,一个过程是一系列指令的序列。
一个函数可以相同,但通常会返回一个结果。


12

有一个术语叫做子程序(subroutine)子程序(subprogram),它指的是一段参数化的代码块,可以从不同的地方调用。

函数和过程是这些的实现。通常情况下,函数会返回值,而过程则不会返回任何内容。


10

这是一个众所周知的老问题,但我想分享一些关于现代编程语言研究和设计的更多见解。

基本答案

传统上(在结构化编程的意义上)和非正式地说,一个过程是一种可重用的结构性构造,用于“输入”并执行可编程操作。当需要在过程内完成某些操作时,您可以在源代码中提供(实际)参数以在过程调用中调用该过程(通常在某种表达式中),并将参数替换为在定义过程时使用的(形式)参数,然后执行过程体中编码的操作。

函数不仅是过程,因为还可以在体中指定返回值作为“输出”。函数调用与过程调用几乎相同,除了您还可以使用函数调用的结果,在语法上(通常作为其他某个表达式的子表达式)。

传统上,过程调用(而不是函数调用)用于表示不需要关注输出,并且必须有副作用以避免调用成为无操作,从而强调了命令式编程范式。许多传统的编程语言如Pascal提供了“过程”和“函数”来区分这种意图上的风格差异。

(需要明确的是,上述“输入”和“输出”是基于函数的语法属性的简化概念。许多语言还支持通过引用/共享将参数传递给参数,以允许用户在调用期间传输编码在参数中的信息。这样的参数甚至可以被称为“输入/输出参数”。此功能基于传递调用中传递的对象的性质,这与过程/函数的特性是正交的。)

然而,如果函数调用的结果不需要,它可以被(至少在逻辑上)忽略,函数定义/函数调用应该与过程定义/过程调用保持一致。像C、C++和Java这样的ALGOL类语言都以这种方式提供“函数”功能:通过将结果类型void编码为传统过程的特殊情况,无需单独提供“过程”功能,从而避免了语言设计中的一些膨胀。

由于提到了SICP,值得注意的是,在RnRS指定的Scheme语言中,一个过程可能需要或不需要返回计算结果。这是传统“函数”(返回结果)和“过程”(不返回任何内容)的结合体,本质上与许多ALGOL类语言的“函数”概念相同(实际上还共享更多的保证,如在调用之前对操作数进行应用评估)。然而,即使在像SRFI-96这样的规范文档中,仍然存在着老式的差异。

我对于语言设计师不再使用规范臃肿的具体原因并不了解,但是根据我的经验来看,似乎“procedure”作为一个独立的特点已经变得不必要了。像 void 类型这样标记应该强调副作用的用法已经足够。对于那些有 C 语言经验的用户来说,这也更加自然,因为 C 语言等类似语言流行了几十年。此外,它也避免了在类似 RnRS 的情况下出现“procedures”实际上是以更广义的意义上的“functions”的尴尬情况。

理论上,可以使用指定的 unit type 作为函数调用结果的类型来指示结果很特殊,从而指定函数。这将传统过程(其中调用结果不感兴趣)与其他过程区分开来。在语言设计上有不同的风格:

  • 在 RnRS 中,只需将不感兴趣的结果标记为“未指定”值(如果语言必须提及,则为未指定类型),就足以忽略它们。
  • 将不感兴趣的结果指定为专用单元类型的值(例如Kernel#inert)也可以起到作用。
  • 当该类型进一步成为一个底部类型时,它可以被(希望)静态验证并防止用作表达式的类型。类似于 ALGOL 的语言中的void类型正是这种技术的一个例子。ISO C11的_Noreturn也是其中更微妙的一个。

进一步阅读

由于传统概念源自数学,有大量黑魔法大多数人不会费心去了解。严格来说,你不太可能像数学书籍那样完全清楚地理解所有事情。计算机科学书籍可能也提供不了太多帮助。

关于编程语言,有几个需要注意的地方:

  • 不同数学分支中的函数并不总是具有相同的含义。不同编程范式中的函数也可能非常不同(甚至有时函数调用的语法看起来很相似)。有时造成差异的原因是相同的,但有时它们并不相同。
    • 通过数学函数对计算进行建模,然后在编程语言中实现底层计算是惯用方法。除非你知道正在谈论什么,否则要小心避免一一映射它们。
  • 不要将模型与被建模实体混淆。
    • 后者只是前者的一种实现。根据上下文(例如感兴趣的数学分支),可以有多个选择。
    • 特别地,将“函数”视为“映射”或笛卡尔积子集的处理方式几乎是荒谬的,就像将自然数视为 Von-Neumann编码的序数(看起来像一堆{{{, {}}...)}}除了某些有限的上下文之外。
  • 在数学上,函数可以是 部分的。不同的编程语言在这里有不同的处理方式。
    • 一些函数式语言可能会尊重函数的完全性,以保证函数调用中的计算始终在有限步骤内终止。然而,这本质上不是图灵完备的,因此计算表达能力较弱,在除类型检查语义(预期为总体)之外,通用语言中并不常见。
    • 如果过程和函数之间的差异很大,是否应该有“总体过程”?嗯...
  • 类似于函数的构造在演算中用于模拟通用计算编程语言的语义(例如lambda演算中的lambda抽象)可以对操作数采用不同的求值策略
    • 在纯演算中的规约以及纯函数式语言中表达式的求值中,没有改变计算结果的副作用。因此,在函数式结构的主体之前不需要评估操作数(因为通过β-等价性和Church-Rosser属性等属性保持定义“相同结果”的不变量)。
    • 然而,许多编程语言在表达式求值期间可能具有副作用。这意味着,严格的求值策略如应用求值与非严格的求值策略如按需调用不同。这很重要,因为如果没有区别,就没有必要将类似于函数的(即与参数一起使用的)宏与(传统的)函数区分开来。但是,根据理论的风味,这仍然可能是一种人为的现象。也就是说,在更广泛的意义上,类似于函数的宏(尤其是卫生的宏)是带有一些不必要限制(语法阶段)的数学函数。没有这些限制,将(一流的)类似于函数的宏视为过程可能是合理的...
    • 对于对此主题感兴趣的读者,请考虑{{link12:一些现代抽象

非常好的理论回答。提供了比OP想要的更多的信息(太多了吗?),因为它涵盖了子程序、协程、续延、部分和全函数以及异常处理。保持简单而聪明。 - Rich Lysakowski PhD

10

基本区别

  • 函数必须返回一个值,但存储过程返回值是可选的:存储过程可以返回0个或n个值。
  • 函数只能有输入参数,而存储过程可以有输入/输出参数。
  • 对于函数,至少需要一个输入参数,但存储过程可以拥有0到n个输入参数。
  • 函数可以从存储过程中调用,而存储过程不能从函数中调用。

高级区别

  • 在存储过程中,可以通过try-catch块处理异常,而在函数中无法使用try-catch块。
  • 我们可以在存储过程中进行事务管理,而在函数中不行。

在SQL中:

  • 存储过程允许在其中使用SELECT和DML(INSERTUPDATEDELETE)语句,而函数只允许使用SELECT语句。
  • 不能在SELECT语句中使用存储过程,但可以在SELECT语句中嵌入函数。
  • 存储过程不能在WHERE(或HAVINGSELECT)块中的SQL语句中使用,但函数可以。
  • 返回表的函数可以被视为另一个行集。这可以在JOIN块中与其他表一起使用。
  • 内联函数可以被认为是带有参数的视图,并且可以在JOIN块和其他行集操作中使用。

6
这个答案非常针对某种语言,而问题是面向所有语言的。这些陈述在一般情况下并不全都正确,但如果您能说明您所断言的语言或环境,那将有所帮助。 - Mogsdad

6
更严格地说,一个函数f遵守这个属性:如果x=y,则f(x)=f(y),即每次使用相同的参数调用它时,它计算相同的结果(因此它不会改变系统的状态)。
因此,rand()或print("Hello")等不是函数,而是过程。而sqrt(2.0)应该是一个函数:无论多少次调用它,都没有可观察的效果或状态改变,并且它始终返回1.41及以上。

3
这种用法与“函数式”编程的上下文相关。请注意,许多(常常是命令式)语言将它们的子程序称为“函数”,但并不要求具备这个属性。 - dmckee --- ex-moderator kitten
2
我并没有暗示编程语言需要这个属性。无论如何,任何语言都可以编写严格函数,并且我认为尽可能多地使用干净函数进行编程是一个好习惯,然后再用一些主过程将它们粘合在一起。 - Ingo

4
如果我们不考虑编程语言,过程通常指一系列必须可靠且幂等地执行以实现特定结果的行为。也就是说,过程基本上是一个算法。
另一方面,函数是程序中相对独立的代码片段。换句话说,函数是过程的具体实现。

3
在大多数情况下:函数返回一个值,而过程不返回。两者都是将代码组合在一起以执行相同任务的片段。
在函数式编程上下文中(其中所有函数都返回值),函数是一个抽象对象:
f(x)=(1+x)
g(x)=.5*(2+x/2)

在这里,f是与g相同的函数,但是是不同的过程。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接