函数式编程 vs 声明式编程 vs 命令式编程

63

我一直习惯于命令式编程,这是通常告诉计算机逐步执行过程以获得最终结果的方式。另一方面,声明式编程只传递输入并期望输出,而不陈述完成任务的具体过程。我感到困惑的是函数式编程。我知道函数式编程是一种将计算视为数学函数求值的编程范式,避免状态和可变数据,并不是一种声明式语言。然而,我仍然无法理解它的工作原理。

让我们以斐波那契数列的执行为例。

命令式编程:

#include<stdio.h> 
#include<conio.h> 
main() 
{ 
  int n,i,c,a=0,b=1; 
  printf("Enter Fibonacci series of nth term : "); 
  scanf("%d",&n); 
  printf("%d %d ",a,b); 
  for(i=0;i<=(n-3);i++) 
  { 
    c=a+b; 
    a=b; 
    b=c;    
  } 
  printf("%d ",c);
  getch(); 
} 

声明式编程:

Give the nth number and it will return the value of the nth number

函数式编程如何工作?另外,如果我的定义有误,请纠正我。请随意评论。


可能是什么是函数式、声明式和命令式编程?的重复内容。 - nawfal
6个回答

130

你举的声明式编程的例子并不是一个真正的程序,所以它不是一个好的例子。

主要区别在于命令式和声明式。函数式是一种特殊的声明式。

C、C++、Java、Javascript、BASIC、Python、Ruby和大多数其他编程语言都是命令式的。通常,如果它有显式循环(for、while、repeat)并且在每次循环中使用显式赋值操作更改变量,那么它就是命令式的。

SQL和XSLT是两个着名的声明式编程语言。标记语言如HTML和CSS也是声明式的,尽管它们通常不能描述任意算法。

这里是一个计算的例子(从一个合适的数据源累加按性别分类的收入),首先是用命令式语言(Javascript)编写的,然后是用声明式语言(SQL)编写的。

命令式编程

var income_m = 0, income_f = 0;
for (var i = 0; i < income_list.length; i++) {
    if (income_list[i].gender == 'M')
        income_m += income_list[i].income;
    else
        income_f += income_list[i].income;
}

注意:

  • 对于将包含累加总数的变量进行显式初始化;
  • 显式循环遍历数据,修改控制变量(i)和每次迭代时的累加总数;
  • 条件语句(if)仅用于在每次迭代中选择代码路径

声明式编程

select gender, sum(income)
from income_list
group by gender;

注意:

  • 你所声明的输出含义中隐含了需要包含运行总和的存储器单元;
  • CPU需要执行的任何循环(例如income_list表上的循环)都是由您声明的输出和源数据结构隐含的;
  • 条件语句(例如SQL中的case)以函数式方式使用,根据输入值指定输出值,而不是选择代码路径。

函数式编程

正如我在上面提到的,SQL的case函数式编程的一个很好的例子,它是声明性编程的一个受限子集,其中通过组合函数来指定所需的计算。

函数是接受输入并返回输出的对象(例如casesum()…)

组成意味着通过指定一个函数的输出作为下一个函数的输入来链接两个或更多函数(通常通过在一个函数内写入另一个函数来实现)。最后,将整个组合(仍然是一个大函数)应用于可用的输入以获得所需的输出。

在这段代码中,我通过组合函数sum()case声明所需的输出。这就是函数式编程:

select 
    sum(case when some_flag = 'X' then some_column
        else some_other_column end)
from
    ...
如果一个语言中只有两个或多个函数的组合及其对输入数据的应用是唯一可用的结构,那么该语言被称为纯函数式语言。在这些语言中,你会注意到完全没有循环、变量赋值和其他通常的命令式语句。
编辑:我建议观看一些 Anjana Vakil 在 JavaScript 中的函数式编程方面的讲座,以更好地了解它是什么。

3
今天仍然非常相关。太棒了!说得好。 - Mike S.
3
这是一个很棒的答案,我终于有一些关于声明式编程是什么的概念了!@Tobia - Akhila
2
那么,在C++中,std::sort(a.begin(),a.end());可以说是一个声明式程序吗? - InQusitive
这些答案让人觉得声明式编程是一种语言属性或者只是写计算机程序的一种风格。像SQL这样的语言被设计为声明式。但是,并没有什么能够阻止你在所谓的命令式语言比如Java中写出声明式代码。 - ultrajohn

7

声称命令式编程与声明式编程之间缺乏后者顺序的错误过度简化是不正确的。

纯函数式编程没有防止表达顺序和实现,而是在操作语义层面上较难表达随机意外顺序。此外,它具有“不要重复自己”(DRY)的优点,这是一种声明性风格(见下文)。

然而,纯函数式编程不能保证声明性高级语义。为了实现这一点,您需要应用正确的声明式与命令式定义。

erroneous oversimplification to claim that(错误过度简化)

correct definition of declarative vs. imperative(正确的声明式与命令式定义)


更新:请参考我在定义声明式编程的其他答案中提供的更详尽的解释。 - Shelby Moore III

4
我找到了一篇有用的解释,来自《使用C#进行XAML编程》:

关于声明式编程

在声明式编程中,源代码以表达所需结果为主,几乎不强调实际实现方式。

关于命令式编程

命令式编程是声明式编程的相反面。如果将声明式编程看作是声明所需结果,命令式编程则被视为编写代表如何实现所需结果的代码行。


4

在函数式编程中,我们通过使用纯函数来构建不可变的程序。这就是纯函数的含义:(我提到纯函数是因为函数式编程基于纯函数)

  • 它仅依赖于提供的输入,而不依赖于任何可能在其评估过程中或调用之间发生变化的隐藏或外部状态。
  • 它不会对其范围之外产生更改,例如修改全局对象或传递的引用参数。

通过避免外部可观察的副作用,纯函数式编程将声明式评估纯函数应用于创建不可变程序的过程中。

简单地说,函数式编程是一种声明性编程范式,可以表达一组操作,而不会透露它们的实现方式或数据如何流动。指令式编程则将计算机程序视为仅仅是一系列自上而下的语句序列,以便计算出结果并改变系统的状态。

以下是指令式编程的一个例子:我们循环遍历一个数组,计算每个元素的平方,并将新值存储在同一数组中。我们正在改变数组的值。此外,对于每个不同的数组,我们都需要定义一个新的for循环,这个循环不可重复使用。

var array= [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 for(let i= 0; i < array.length; i++) {
     array[i]=Math.pow(array[i], 2);
  }
array; //-> [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

在JavaScript中,我会使用Array.map()方法。

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].map((i)=>i*i)

在这种方法中,我们不知道Array.map()的实现方式。程序的定义与评估是分开的。更重要的是,这段代码不会改变原始数组,而是创建一个新的数组。
另一个很好的声明式编程示例是编写SQL查询语句。我们只需要编写一个简单的SQL语句从数据库中获取数据,但我们不知道背后发生了什么,所有的魔法都被抽象化了。

1

想一下C语言中的过滤器。你从标准输入读取数据,然后将其写入标准输出。虽然代码可能是命令式的,但程序像一个函数一样使用。比如你有一个程序 'function',然后通过管道传输:

cat foo  |function |tee bar

将会把foo文件的内容通过function进行过滤,然后再通过tee过滤器同时写入标准输出和创建bar文件。同样地,grepawk也是这样的迭代器,它们被用作函数。


1

有一种观点认为,存在声明式语言和命令式语言之分是一种谬论。

在Java中,你所做的每一件事情都是对编译器的意图声明,告诉它你需要字节码实现什么。你不关心它是如何实现的。但是字节码本身是对JIT编译器的意图声明,告诉它你想要机器指令实现什么。你不关心它是如何实现的。

即使是汇编语言也不是真正的命令式语言。CPU解释机器代码并对其进行各种优化和转换。除了希望它非常快之外,编写汇编代码的人并不关心AMD核心和IBM核心将以不同的方式执行机器指令。

另一方面,每次调用库函数都是对库编写者的意图声明,告诉它你想要调用实现什么,但你通常不关心它是如何实现的。反过来,你的代码本身可能被用来满足其他人的意图声明,而他们并不关心你是如何实现的。

相反,简单的SQL语句无法满足复杂的需求(这并不奇怪)。为了得到想要的结果,你必须使用大量嵌套的子查询(如果你是一个受虐狂),或者将中间结果写入临时表,并对其应用进一步的查询集,每个查询集可能需要更多的临时表上的查询(如果你是一个实用主义者)。这两种方法是等效的。而无论采取哪种方法,它都代表了一系列“命令式”的指令。

实际上,“声明式”只是意味着“非常高级”。但为了得到你实际需要的东西,你经常需要降到一个较低的级别。同样,Java API本身可能模拟所谓的声明性语言(database.select().from(table1, table2).where(<lambda>).groupBy(<lambda>)).having(<lambda>))。但当它被写成这样时,它看起来就不那么“声明式”了,是吧?它看起来非常像你在链接命令式指令。

这个例子展示了Java作为所谓的函数式语言的用法。那么SQL是函数式的吗?Java版本看起来几乎相同,功能也相同。但它是用"命令式"语言编写的。那么SQL是命令式的吗?Java是函数式的吗?声明式的吗?

语言开发人员和语言倡导者喜欢将他们的语言呈现为具有某些特性,使其听起来与主流语言根本不同,并具有独特的问题解决哲学方法。但通常只要深入挖掘一下,就会发现更加平凡。

我不会纠结于命令式、函数式和声明式的含义。每种语言都是声明式的,命令式的和函数式的。找到适合您口味的编码风格,并使用适合您风格的语言。


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