函数式和命令式编程语言之间有什么区别?


159

大多数主流语言,包括诸如C#,Visual Basic,C ++和Java之类的面向对象编程(OOP)语言,都被设计为主要支持命令式(过程式)编程,而类似Haskell / gofer的语言纯粹是功能性的。谁能详细说明这两种编程方式之间的区别?

我知道选择编程方式取决于用户要求,但是为什么建议学习功能性编程语言呢?



1
检查其他[post] [1]。它清楚地描述了差异。[1]:stackoverflow.com/questions/602444/…–
theta

Answers:


160

定义: 一种命令式语言使用一系列语句来确定如何实现某个目标。据说这些语句会改变程序的状态,因为依次执行每个语句。

示例: Java是命令式语言。例如,可以创建一个程序来添加一系列数字:

 int total = 0;
 int number1 = 5;
 int number2 = 10;
 int number3 = 15;
 total = number1 + number2 + number3; 

每个语句都会更改程序的状态,从为每个变量分配值到最终添加这些值。通过使用五个语句序列,可以明确地告诉程序如何将数字5、10和15相加。

函数式语言: 函数式编程范式已明确创建,以支持解决问题的纯函数式方法。函数式编程是声明式编程的一种形式。

纯函数的优点: 将函数转换实现为纯函数的主要原因是纯函数是可组合的:即自包含且无状态。这些特征带来了许多好处,包括:增强的可读性和可维护性。这是因为每个函数都设计为在给定参数的情况下完成特定任务。该功能不依赖任何外部状态。

简化迭代开发。由于代码易于重构,因此对设计的更改通常更易于实现。例如,假设您编写了一个复杂的转换,然后意识到某些代码在转换中重复了几次。如果通过纯方法进行重构,则可以随意调用纯方法而不必担心副作用。

简化测试和调试。因为可以更轻松地对纯函数进行隔离测试,所以您可以编写测试代码,以典型值,有效边缘情况和无效边缘情况来调用纯函数。

对于OOP People或命令式语言:

当您对事物进行固定的操作,并且随着代码的发展,您主要添加新的事物时,面向对象的语言会很好。这可以通过添加实现现有方法的新类来完成,而现有类则不予考虑。

当您拥有固定的事物集并且随着代码的发展,主要是在现有事物上添加新的操作时,功能语言会很好。这可以通过添加新功能来实现,这些新功能可以使用现有数据类型进行计算,而现有功能则可以单独使用。

缺点:

选择编程方式取决于用户要求,因此只有在用户未选择正确方式时才有危害。

当进化走错路时,您就会遇到问题:

  • 向面向对象的程序添加新操作可能需要编辑许多类定义以添加新方法
  • 向功能程序中添加新事物可能需要编辑许多功能定义以添加新案例。

10
在这种情况下,纯函数等效于数学函数。相同的输入将始终映射到相同的输出。它们也没有任何副作用(除了返回一个或多个值之外),这意味着编译器可以进行一些很酷的优化,并且它可以并行运行函数,因为没有什么可争的。
WorBlux

那么,组成可维护和可测试的oop应用程序的正确方法和最佳实践往往是在以一种令人愉悦的心态设计命令式代码?
凯末尔·居尔特金2014年

4
在突出显示每个编程功能的文字上,我没有看到明显的区别。程序性编程的大多数描述都可以由命令性编程文本交换,反之亦然。
AxeEffect 2015年

7
这个答案试图澄清什么是函数编程,但是根本不用去定义什么是纯函数。我看不到有人能读懂这个答案,而对知道声明式和过程式编程之间的区别充满信心。
Ringo

230

区别在于:

当务之急:

  • 开始
  • 打开9 1/2号鞋子。
  • 在口袋里腾出空间来存放钥匙[7]。
  • 将钥匙放在房间里,把钥匙放在口袋里。
  • 进入车库。
  • 打开车库。
  • 输入汽车。

...依此类推...

  • 将牛奶放入冰箱。
  • 停止。

声明性的,其功能是子类别:

  • 牛奶是一种健康的饮料,除非您在消化乳糖时遇到问题。
  • 通常,人们将牛奶储存在冰箱中。
  • 冰箱是一个盒子,可以保持里面的物品凉爽。
  • 商店是出售物品的地方。
  • “销售”是指物有所值。
  • 同样,将货币交换为事物也称为“购买”。

...依此类推...

  • 确保冰箱里有牛奶(当需要时-用于懒惰的功能语言)。

摘要:使用命令式语言,您可以告诉计算机如何更改内存中的位,字节和单词以及更改顺序。在功能方面,我们告诉计算机什么是事物,动作等。例如,我们说0的阶乘为1,其他所有自然数的阶乘是该数与其前身的阶乘的乘积。我们不是说:要计算n的阶乘,请保留一个内存区域并在其中存储1,然后将该内存区域中的数字乘以2到n,然后将结果存储在同一位置,最后,存储区域将包含阶乘。


1
谢谢。那是查看它的好方法。
L-Samuels 2014年

5
我喜欢您的解释@Igno,但是我仍然不清楚。在声明式中,即使您只是讲一些东西,但仍然需要更改位并更改机器中的状态才能正确进行。让我感到困惑的是,声明式在某种程度上类似于过程式编程(如C函数),但它们之间在内部仍存在很大差异。C函数函数编程(在机器级别)上的函数不相同吗?
phoenisx '16

11
@Igno,像Subroto一样,我不太了解您的解释。您写的内容似乎可以总结为:需要答案...获取答案。似乎忽略了重要的方面。我不明白您如何才能仅向用户隐藏该部分,在某些时候某人必须知道它是如何完成的……您无法永远将向导置于幕后。
布雷特·托马斯

3
这并不是我理解的函数式编程。我认为函数式编程是从函数中删除隐藏的输入和输出。
林戈

7
令人费解的解释。
JoeTidee '17

14

大多数现代语言在不同程度上都具有命令式和函数式的功能,但是为了更好地理解函数式编程,最好使用像Haskell这样的纯函数式语言的示例,而不是像Java / c#这样的功能性语言中的命令式代码。我相信通过示例进行解释总是很容易的,因此下面是一个示例。

功能编程:计算n的阶乘,即n!即nx(n-1)x(n-2)x ... x 2 X 1

-- | Haskell comment goes like
-- | below 2 lines is code to calculate factorial and 3rd is it's execution  

factorial 0 = 1
factorial n = n * factorial (n - 1)
factorial 3

-- | for brevity let's call factorial as f; And x => y shows order execution left to right
-- | above executes as := f(3) as 3 x f(2) => f(2) as 2 x f(1) => f(1) as 1 x f(0) => f(0) as 1  
-- | 3 x (2 x (1 x (1)) = 6

注意,Haskel允许函数重载到参数值的级别。现在下面是命令式代码的示例,它具有越来越高的命令性:

//somewhat functional way
function factorial(n) {
  if(n < 1) {
     return 1;
  }
  return n * factorial(n-1);   
}
factorial(3);

//somewhat more imperative way
function imperativeFactor(n) {
  int f = 1
  for(int i = 1; i <= n; i++) {
     f = f * i
  }
  return f;
}

阅读资料可以很好地理解命令式代码如何更多地关注零件,机器状态(即for循环),执行顺序和流程控制。

后面的示例可以大致看作是Java / c#lang代码,而第一部分可以看作是语言本身的局限性,与Haskell会将函数按值(零)重载形成对比,因此,可以说这不是纯粹的函数式语言。你可以说它支持功能编。在某种程度上。

披露:以上代码均未经过测试/执行,但希望能够足以传达这一概念;我也将不胜感激任何这种纠正意见:)


1
不是return n * factorial(n-1);吗?
jinawee

@jinawee,感谢您指出,我已将其改正自n * (n-1)
old-monk

10

函数式编程是声明性编程的一种形式,它描述了计算的逻辑,并且完全不强调执行的顺序。

问题:我想把这种生物从马变成长颈鹿。

  • 延长脖子
  • 延长腿
  • 涂点
  • 给该生物以黑色的舌头
  • 去除马尾

每个项目可以以任何顺序运行以产生相同的结果。

命令式编程是程序性的。状态和秩序很重要。

问题:我想停车。

  1. 注意车库门的初始状态
  2. 将车停在车道上
  3. 如果车库门关闭,则打开车库门,记住新的状态;否则继续
  4. 把车拉进车库
  5. 关上车库门

必须完成每个步骤才能获得所需的结果。在车库门关闭时将其拉入车库会导致车库门损坏。


我只看到异步与同步的区别。
Vladimir Vukanac

@VladimirVukanac异步/同步是一种机制,而不是编程的形式
Jakub Keller

2
哦,谢谢,我将对此进行更多研究。您是否愿意将问题1更新为与问题2“我想停放我的车”相同,但是以函数式编程方式编写?然后将排除并行性。
Vladimir Vukanac

6

函数式编程是“使用函数编程”,其中函数具有某些预期的数学特性,包括引用透明性。从这些特性出发,进一步的特性流动,特别是由可替代性实现的熟悉的推理步骤,这些步骤导致了数学证明(即证明结果的可信度)。

由此可见,功能程序仅是一种表达。

通过注意命令式程序中表达式不再是引用透明的位置(因此不是用函数和值构建的,并且本身不能成为函数的一部分)的位置,可以轻松查看两种样式之间的对比。最明显的两个地方是:突变(例如变量)其他副作用非本地控制流(例如例外)

在由功能和值组成的按程序表示的框架上,构建了语言,概念,“功能模式”,组合器以及各种类型的系统和评估算法的完整实用范例。

按照最极端的定义,几乎所有语言(甚至是C或Java)都可以称为功能性语言,但通常人们会保留具有特定相关抽象性(例如闭包,不变值和句法辅助工具(例如模式匹配))的术语。就使用函数式编程而言,它涉及使用functins并构建代码而没有任何副作用。用来写证明


3

从2005年到2013年,命令式编程风格一直在Web开发中得到实践。

通过命令式编程,我们逐步地写出了代码,该代码确切地列出了我们的应用程序应执行的操作。

函数式编程风格通过巧妙地组合函数来产生抽象。

答案中提到了声明式编程,关于这一点,我将说声明式编程列出了我们要遵循的一些规则。然后,我们向我们的应用程序提供所谓的某种初始状态,然后让这些规则定义应用程序的行为方式。

现在,这些快速描述可能没有多大意义,因此让我们通过类推来逐步了解命令式和声明式编程之间的区别。

想象一下,我们不是在构建软件,而是在烤馅饼。也许我们是糟糕的面包师,不知道如何以我们应该的方式烘烤美味的馅饼。

因此,我们的老板给了我们一份方向清单,这就是我们所知道的食谱。

食谱将告诉我们如何做馅饼。一种配方以命令式编写,如下所示:

  1. 混合1杯面粉
  2. 加一个鸡蛋
  3. 加1杯糖
  4. 将混合物倒入锅中
  5. 将平底锅放在烤箱中30分钟和350华氏度。

声明式配方将执行以下操作:

1杯面粉,1个鸡蛋,1杯糖-初始状态

规则

  1. 如果一切混合在一起,则放入锅中。
  2. 如果一切都没有混合,放在碗里。
  3. 如果一切都平底锅,放在烤箱中。

因此,命令式方法的特征在于逐步方法。您从第一步开始,然后转到第二步,依此类推。

您最终会得到一些最终产品。因此,制作这块馅饼时,我们将这些配料混合在一起,放入锅中和烤箱中,即可得到最终产品。

在声明性世界中,它是不同的。在声明性配方中,我们将我们的配方分为两个独立的部分,从列出配方初始状态(如变量)的一个部分开始。因此,这里的变量是成分的数量及其类型。

我们采用初始状态或初始成分,并对它们应用一些规则。

因此,我们采用初始状态,并一遍又一遍地通过这些规则,直到可以立即食用大黄草莓派或其他任何东西为止。

因此,在声明式方法中,我们必须知道如何正确构造这些规则。

因此,我们可能要检查成分或状态的规则(如果混合),将它们放入锅中。

在我们的初始状态下,这是不匹配的,因为我们尚未混合成分。

因此规则2说,如果不混合,则将其混合在碗中。好的,这条规则适用。

现在我们有一碗混合配料作为我们的状态。

现在,我们再次将该新状态应用于规则。

因此,规则1指出,如果将各种配料混合在一起放在锅中,是的,现在规则1确实适用,那就去做吧。

现在,我们有了一个新的状态,可以在锅中将原料混合在一起。规则1不再相关,规则2不适用。

规则3说,如果将食材放在锅中,将它们放入烤箱,那么该规则就是适用于这种新状态的,那就做吧。

最后我们得到了美味的热苹果派或其他任何东西。

现在,如果您像我一样,您可能正在思考,为什么我们仍不进行命令式编程。这是有道理的。

是的,对于简单的流程,是的,但是大多数Web应用程序具有更复杂的流程,而命令式编程设计无法正确捕获这些流程。

在声明性方法中,我们可能具有一些初始成分或初始状态textInput=“”,例如单个变量。

也许文本输入开始时是一个空字符串。

我们采用此初始状态,并将其应用于您的应用程序中定义的一组规则。

  1. 如果用户输入文本,请更新文本输入。好吧,现在那不适用。

  2. 如果呈现了模板,请计算小部件。

  3. 如果textInput已更新,请重新呈现模板。

嗯,这都不适用,因此程序将只等待事件发生。

因此,有时用户会更新文本输入,然后我们可能会应用规则编号1。

我们可能会将其更新为 “abcd”

所以我们只是更新了text和textInput更新,规则2不适用,规则3说如果是刚刚发生的文本输入更新,则重新渲染模板,然后回到规则2,即是否渲染了模板,计算窗口小部件,好的,让我们计算窗口小部件。

总的来说,作为程序员,我们想争取更具声明性的编程设计。

强制性命令看起来更清晰明显,但是声明性方法可以很好地扩展到较大的应用程序。


2

•命令式语言:

  • 高效执行

  • 复杂的语义

  • 复杂语法

  • 并发是程序员设计的

  • 复杂的测试,没有参照透明性,有副作用

  • 有状态

•功能语言:

  • 简单的语义

  • 简单语法

  • 执行效率较低

  • 程序可以自动并发

  • 测试简单,具有参照透明性,无副作用

  • 没有状态

1

我认为有可能以命令式方式表达函数式编程:

  • 使用很多状态检查对象和if... else/ switch语句
  • 一些超时/等待机制来照顾异步

这种方法存在很大的问题:

  • 重复规则/程序
  • 有状态性会带来副作用/错误的机会

解决诸如我认为的那些问题的功能编程,像对象一样对待函数/方法,并拥抱无状态。

用法示例:前端应用程序,例如Android,iOS或Web应用程序的逻辑(包括)。与后端通信。

使用命令性/过程代码模拟功能编程时的其他挑战:

  • 比赛条件
  • 事件的复杂组合和顺序。例如,用户尝试在银行应用中汇款。步骤1)并行执行以下所有操作,只有在一切都很好的情况下才继续进行a)检查用户是否仍然良好(欺诈,AML)b)检查用户是否有足够的余额c)检查收件人是否有效且良好(欺诈, AML)等。步骤2)执行转移操作。步骤3)显示用户余额和/或某种形式的跟踪更新。以RxJava为例,代码简洁明了。没有它,我可以想象会有很多代码,凌乱且容易出错的代码

我也相信,归根结底,功能代码将被编译器编译为命令式或过程式的汇编或机器代码。但是,除非您编写汇编程序,否则就像人类用高级/人类可读语言编写代码一样,对于列出的场景,函数式编程是更合适的表达方式。


-1

我知道这个问题比较老,其他人已经很好地解释了,我想举一个例子问题,用简单的术语解释同样的问题。

问题:写1的表。

解决方案:-

通过命令式:=>

    1*1=1
    1*2=2
    1*3=3
    .
    .
    .
    1*n=n 

按功能样式:=>

    1
    2
    3
    .
    .
    .
    n

命令式的说明我们更明确地编写指令,并且可以更简化的方式进行调用。

与功能样式一样,不言而喻的东西将被忽略。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.