函数式编程语言和命令式编程语言的区别是什么?

大多数主流语言,包括面向对象编程(OOP)语言,如c#、Visual Basic、c++和Java,主要是设计来支持命令式(过程式)编程,而Haskell/gofer之类的语言是纯函数式的。有人能详细说明一下这两种编程方式的区别吗?

我知道要根据用户的需求来选择编程的方式,但是为什么推荐学习函数式编程语言呢?

172563 次浏览

< >强定义: 命令式语言使用语句序列来确定如何达到某个目标。这些语句在依次执行时改变程序的状态

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

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

每条语句改变程序的状态,从给每个变量赋值到最终添加这些值。使用一个由五个语句组成的序列,程序被显式地告知如何将数字5、10和15相加。

< >强函数式语言: 函数式编程范式被明确地创建来支持解决问题的纯函数方法。函数式编程是声明式编程的一种形式

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

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

更容易的测试和调试。因为纯函数可以更容易地进行隔离测试,所以您可以编写使用典型值、有效边界用例和无效边界用例调用纯函数的测试代码。

For OOP People or 命令式语言:< /强> < / p >

当你对事物有一组固定的操作,并且随着代码的发展,你主要是添加新的东西时,面向对象语言是很好的。这可以通过添加实现现有方法的新类来实现,而现有类则保持不变。

当你有一组固定的东西,并且随着代码的发展,你主要是在现有的东西上添加新的操作时,函数式语言是很好的。这可以通过添加新的函数来实现,这些函数使用现有的数据类型进行计算,而现有的函数保持不变。

缺点:

编程方式的选择取决于用户的需求,只有用户选择不正确的方式才会有危害。

当进化走错了路,你就会遇到问题:

  • 向面向对象程序添加新操作可能需要编辑许多类定义来添加新方法
  • 向函数式程序中添加一种新的东西可能需要编辑许多函数定义来添加一个新的case。

区别在于:

必要的:

  • 开始
  • 穿上9号半的鞋子。
  • 在你的口袋里腾出空间来放一个数组的键。
  • 把钥匙放在房间里,因为钥匙在口袋里。
  • 进入车库。
  • 打开车库。
  • 进入汽车。

... 诸如此类……

  • 把牛奶放进冰箱。
  • 停止。

声明性的,其中functional是子类别:

  • 牛奶是一种健康饮料,除非你消化乳糖有问题。
  • 通常,人们把牛奶储存在冰箱里。
  • 冰箱是一个盒子,可以让里面的东西保持凉爽。
  • 商店是出售商品的地方。
  • 这里的“出售”指的是用物品交换金钱。
  • 此外,金钱交换物品被称为“购买”。

... 诸如此类……

  • 确保冰箱里有牛奶(当我们需要牛奶的时候——对于懒惰函数式语言)。

概要:在命令式语言中,你告诉计算机如何改变内存中的比特、字节和单词,以及以什么顺序。在功能模型中,我们告诉计算机事物、动作等是什么。例如,我们说0的阶乘是1,每一个其他自然数的阶乘是这个数和它的前一个自然数的阶乘的乘积。我们不会说:为了计算n的阶乘,预留一个内存区域并将1存储在那里,然后将该内存区域中的数字与数字2到n相乘,并将结果存储在相同的位置,最后,内存区域将包含阶乘。

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

问题:写1的表。

解决方案:

通过命令式样式:=>

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

By Functional style: =>

    1
2
3
.
.
.
n

在命令式的解释中,我们更明确地编写指令,并且可以以更简化的方式调用。

而在函数式风格中,不言自明的东西将被忽略。

大多数现代语言在不同程度上都是命令式和函数式的,但为了更好地理解函数式编程,最好以纯函数式语言(如Haskell)为例,与非函数式语言(如java/ c#)中的命令式代码进行对比。我相信用例子来解释总是很容易的,下面就是一个例子。

函数式编程:计算n的阶乘,即n!即n x (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;
}

这个可以作为一个很好的参考来理解如何命令代码更关注如何部分,机器状态(i in for循环),执行顺序,流控制。

后面的例子可以被视为java/ c# lang代码,第一部分是语言本身的限制,与Haskell相比,它通过值(零)重载函数,因此可以说它不是纯粹的函数式语言,另一方面,你可以说它支持函数式prog。在某种程度上。

披露:上面的代码都没有测试/执行,但希望应该足够好,以传达的概念;我也很感激任何这样的纠正意见:)

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

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

  • 延长的脖子
  • 延长腿
  • 应用斑点
  • 给这个生物一个黑色的舌头
  • 去除马尾

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

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

问题:我想停车。

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

为了达到预期的结果,每一步都必须完成。当车库门关着的时候开进车库会导致车库门坏掉。

我认为可以用命令式的方式来表达函数式编程:

  • 使用大量的对象状态检查和if... else/ switch语句
  • 一些超时/等待机制来处理异步性

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

  • 规则/程序重复
  • 状态性给副作用/错误留下了机会

函数式编程,像对待对象一样对待函数/方法,并接受无状态,我相信这是为了解决这些问题而诞生的。

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

使用命令式/过程式代码模拟函数式编程时的其他挑战:

  • 竞态条件
  • 事件的复杂组合和顺序。例如,用户试图在银行应用程序中汇款。步骤1)并行执行以下所有操作,只有在一切正常的情况下才能继续执行a)检查用户是否仍然正常(欺诈,AML) b)检查用户是否有足够的余额c)检查收件人是否有效且良好(欺诈,AML)等。步骤2)执行转账操作步骤3)显示用户余额的更新和/或某种跟踪。以RxJava为例,代码简洁而合理。如果没有它,我可以想象会有大量的代码,混乱和容易出错的代码

我也相信,在一天结束的时候,函数式代码会被编译器转换成命令式/过程式的汇编代码或机器代码。但是,除非您编写汇编,就像使用高级/人类可读的语言编写代码一样,函数式编程是对于列出的场景更合适的表达方式

命令式编程风格从2005年一直应用到2013年的web开发中。

使用命令式编程,我们编写出代码,逐级列出应用程序应该做什么。

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

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

现在,这些简短的描述可能没有多大意义,所以让我们通过一个类比来了解命令式编程和声明式编程之间的区别。

想象一下,我们不是在开发软件,而是在以烤馅饼为生。也许我们是糟糕的烘焙师,不知道如何以我们应该的方式烘焙美味的派。

所以我们的老板给了我们一张食谱的清单,也就是我们所说的食谱。

这个食谱会告诉我们如何做派。一个recipe是用命令式写的,像这样:

  1. 混合一杯面粉
  2. 加入1个鸡蛋
  3. 加一杯糖
  4. 把混合物倒进锅里
  5. 把平底锅放进烤箱烤30分钟,温度达到华氏350度。

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

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

规则

  1. 如果所有东西都混合在一起,放入锅中。
  2. 如果所有东西都没有混合,放在碗里。
  3. 如果所有东西都在平底锅里,那么就放在烤箱里。

命令式方法的特点是循序渐进的方法。你从第一步开始,然后到第二步,以此类推。

最终会得到最终产物。做这个派,我们把这些原料混合,放在平底锅里,放进烤箱,你就得到了最终的成品。

在声明式的世界里,情况就不同了。在声明式配方中,我们将配方分为两个单独的部分,从列出配方的初始状态(如变量)的一部分开始。这里的变量是原料的数量和种类。

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

所以我们取初始状态,一遍又一遍地让它们遵循这些规则,直到我们准备好吃大黄草莓派或其他东西。

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

规则是,我们可能要检查我们的原料或状态,如果混合了,把它们放在锅里。

在初始状态下,不匹配因为我们还没有混合原料。

规则2说,如果它们没有混合,就把它们放在碗里混合。好的,这条规则适用。

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

现在我们再次将新状态应用到规则中。

规则一说,如果配料混合了把它们放在锅里,好的,现在规则一适用了,我们来做吧。

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

规则3说,如果食材在平底锅里,把它们放在烤箱里,很好,这个规则适用于这个新状态,让我们这样做。

最后我们就能吃到美味的热苹果派之类的。

现在,如果你和我一样,你可能会想,为什么我们不再做命令式编程了。这是有道理的。

嗯,对于简单的流来说是可以的,但是大多数web应用程序都有更复杂的流,命令式编程设计不能正确地捕获这些流。

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

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

我们获取这个初始状态,并将其应用于应用程序中定义的一组规则。

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

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

  3. 如果textInput被更新,重新渲染模板。

好吧,这些都不适用,所以程序只会等待事件发生。

在某个时刻,用户更新了文本输入,然后我们可能应用规则1。

我们可以将其更新为“abcd”

我们刚刚更新了text和textInput更新,规则2不适用,规则3说,如果text输入是更新的,就重新渲染模板,然后回到规则2,说,如果模板被渲染,就计算小部件,好,我们来计算小部件。

一般来说,作为程序员,我们希望争取更多的声明性编程设计。

命令式方法似乎更清楚和明显,但声明式方法非常适合大型应用程序。

//The IMPERATIVE way
int a = ...
int b = ...


int c = 0; //1. there is mutable data
c = a+b;   //2. statements (our +, our =) are used to update existing data (variable c)

命令式程序=改变现有数据的语句序列。

关注WHAT =我们正在变化的数据(可修改的值也就是变量)。

要链接命令式语句=使用过程(和/或oop)。


//The FUNCTIONAL way
const int a = ... //data is always immutable
const int b = ... //data is always immutable


//1. declare pure functions; we use statements to create "new" data (the result of our +), but nothing is ever "changed"
int add(x, y)
{
return x+y;
}


//2. usage = call functions to get new data
const int c = add(a,b); //c can only be assigned (=) once (const)

一个功能程序=一列功能“解释”;如何获得新的数据。

关注HOW =我们的函数add

链接功能“语句”;=使用函数组合。


这些基本的区别有着深刻的含义。

严肃的软件有大量的数据和代码。

因此相同的数据(变量)被用于代码的多个部分。

A.在命令式程序中,这种(共享)数据的可变性会导致问题

  • 代码很难理解/维护(因为数据可以在不同的位置/方式/时刻修改)
  • 并行化代码是很困难的(一次只有一个线程可以改变内存位置),这意味着改变对同一个变量的访问必须被序列化=开发人员必须编写额外的代码来强制对共享资源的序列化访问,通常是通过锁/信号量

作为一个优点:数据是真正修改到位,较少需要复制。(一些性能提升)

B.另一方面,函数式代码使用不可变数据,不会有这样的问题。数据是只读的,所以没有竞争条件。代码可以很容易地并行化。结果可以被缓存。更容易理解。

缺点是:为了得到“修改”,数据被大量复制。

参见:https://en.wikipedia.org/wiki/Referential_transparency

关于什么是函数式程序,什么是命令式程序,似乎有很多观点。

我认为函数式程序最容易被描述为“懒惰求值”。导向。通过设计语言采用递归方法,而不是让程序计数器遍历指令。

在函数式语言中,函数的求值将从返回语句开始并回溯,直到最终达到一个值。这对语言语法有深远的影响。

命令:发送计算机

下面,我试图用邮局的比喻来说明这一点。命令式语言会将计算机发送给不同的算法,然后让计算机返回一个结果。

功能性:运送食谱

函数式语言将发送食谱,当你需要一个结果时,计算机将开始处理食谱。

通过这种方式,您可以确保不会浪费太多CPU周期来执行从未用于计算结果的工作。

当你用函数式语言调用一个函数时,返回值是一个由食谱组成的食谱,而这个食谱又由食谱组成。这些食谱实际上就是所谓的闭包

// helper function, to illustrate the point
function unwrap(val) {
while (typeof val === "function") val = val();
return val;
}


function inc(val) {
return function() { unwrap(val) + 1 };
}


function dec(val) {
return function() { unwrap(val) - 1 };
}


function add(val1, val2) {
return function() { unwrap(val1) + unwrap(val2) }
}


// lets "calculate" something


let thirteen = inc(inc(inc(10)))
let twentyFive = dec(add(thirteen, thirteen))


// MAGIC! The computer still has not calculated anything.
// 'thirteen' is simply a recipe that will provide us with the value 13


// lets compose a new function


let doubler = function(val) {
return add(val, val);
}


// more modern syntax, but it's the same:
let alternativeDoubler = (val) => add(val, val)


// another function
let doublerMinusOne = (val) => dec(add(val, val));


// Will this be calculating anything?


let twentyFive = doubler(thirteen)


// no, nothing has been calculated. If we need the value, we have to unwrap it:
console.log(unwrap(thirteen)); // 26

unwrap函数将对所有函数求值,直到有一个标量值为止。

语言设计结果

命令式语言中的一些很好的特性,在函数式语言中是不可能的。例如value++表达式,在函数式语言中很难求值。函数式语言对语法必须如何进行约束,因为它们的求值方式不同。

另一方面,命令式语言可以从函数式语言中借鉴伟大的思想并成为混合体。

函数式语言对于一元操作符有很大的困难,例如++来增加一个值。这个困难的原因并不明显,除非你明白函数式语言的计算方法是“在reverse"

实现一元运算符必须像这样实现:

let value = 10;


function increment_operator(value) {
return function() {
unwrap(value) + 1;
}
}


value++ // would "under the hood" become value = increment_operator(value)


注意,我上面使用的unwrap函数,是因为javascript不是函数式语言,所以在需要时,我们必须手动展开值。

现在很明显,应用1000次增量会导致我们用10000个闭包来包装这个值,这是毫无价值的。

更明显的方法是实际上直接改变值在适当的位置 -但是瞧:你已经引入了修改的值,也就是可变值,这使得语言必要的 -或者实际上是一种混合。

实际上,当提供输入时,它可以归结为两种不同的输出方法。

下面,我将尝试用以下项目来描绘一个城市:

  1. 电脑
  2. 你的家
  3. 斐波那契的

命令式语言

任务:计算第三斐波那契数数字。 步骤:< / p >

  1. 电脑放入一个框中,并用便利贴标记它:

    价值
    邮件地址 The Fibonaccis
    返回地址 Your Home
    参数 3
    返回值 undefined

    and send off computer

  2. 斐波那契的将在收到盒子后像往常一样做:

    • 是参数<2?

      • 是:更改便利贴,并将计算机返回到邮局:

        价值
        邮件地址 The Fibonaccis
        返回地址 Your Home
        参数 3
        返回值 01 (返回参数)

        and return to sender. .

      • < p >否则:

        1. 将一个新的便利贴放在旧的便利贴上面:

          价值
          邮件地址 The Fibonaccis
          返回地址 < >强Otherwise, step 2, c/o < / >强The Fibonaccis
          参数 2 (第一个参数传递)
          返回值 undefined

          并发送

        2. 去掉返回的便利贴。将一个新的便利贴放在初始的便利贴之上,并再次发送计算机:

          价值
          邮件地址 The Fibonaccis
          返回地址 < >强Otherwise, done, c/o < / >强 The Fibonaccis
          参数 2 (通过parameter-2)
          返回值 undefined
          李< / div > < / >
        3. 到现在为止,我们应该从请求者那里得到了便利贴的初始值,其中两个使用了便签,每个返回值字段都已填充。我们总结返回值并将其放入最终便利贴的return Value字段中。

          价值
          邮件地址 The Fibonaccis
          返回地址 Your Home
          参数 3
          返回值 2 (returnValue1 + returnValue2)

          and return to sender. .

正如您可以想象的那样,在您将计算机发送到您调用的函数之后,许多工作立即开始。

整个编程逻辑是递归的,但实际上,当计算机在便签堆栈的帮助下从一个算法移动到另一个算法时,算法是顺序发生的。

函数式语言

任务:计算第三斐波那契数数字。步骤:

  1. 在便利贴上写下以下内容:

    价值
    指令 The Fibonaccis
    参数 3.
    李< / div > < / >

这是本质上它。该便利贴现在表示fib(3)的计算结果。

我们已经将参数3附加到名为The Fibonaccis的配方中。计算机不需要执行任何计算,除非有人需要标量值。

函数式Javascript示例

我一直致力于设计一种名为魅力的编程语言,这就是fibonacci在该语言中的样子。

fib: (n) => if (
n < 2               // test
n                   // when true
fib(n-1) + fib(n-2) // when false
)
print(fib(4));

这段代码可以被编译成命令式和功能性的“字节码”。

javascript的命令式版本是:

let fib = (n) =>
n < 2 ?
n :
fib(n-1) + fib(n-2);

半功能的javascript版本是:

let fib = (n) => () =>
n < 2 ?
n :
fib(n-1) + fib(n-2);

PURE的函数式javascript版本会涉及更多的内容,因为javascript没有函数式的等价物。

let unwrap = ($) =>
typeof $ !== "function" ? $ : unwrap($());


let $if = ($test, $whenTrue, $whenFalse) => () =>
unwrap($test) ? $whenTrue : $whenFalse;


let $lessThen = (a, b) => () =>
unwrap(a) < unwrap(b);


let $add = ($value, $amount) => () =>
unwrap($value) + unwrap($amount);


let $sub = ($value, $amount) => () =>
unwrap($value) - unwrap($amount);


let $fib = ($n) => () =>
$if(
$lessThen($n, 2),
$n,
$add( $fib( $sub($n, 1) ), $fib( $sub($n, 2) ) )
);


我将手动“编译”;它变成javascript代码:

"use strict";


// Library of functions:
/**
* Function that resolves the output of a function.
*/
let $$ = (val) => {
while (typeof val === "function") {
val = val();
}
return val;
}


/**
* Functional if
*
* The $ suffix is a convention I use to show that it is "functional"
* style, and I need to use $$() to "unwrap" the value when I need it.
*/
let if$ = (test, whenTrue, otherwise) => () =>
$$(test) ? whenTrue : otherwise;


/**
* Functional lt (less then)
*/
let lt$ = (leftSide, rightSide)   => () =>
$$(leftSide) < $$(rightSide)




/**
* Functional add (+)
*/
let add$ = (leftSide, rightSide) => () =>
$$(leftSide) + $$(rightSide)


// My hand compiled Charm script:


/**
* Functional fib compiled
*/
let fib$ = (n) => if$(                 // fib: (n) => if(
lt$(n, 2),                           //   n < 2
() => n,                             //   n
() => add$(fib$(n-2), fib$(n-1))     //   fib(n-1) + fib(n-2)
)                                      // )


// This takes a microsecond or so, because nothing is calculated
console.log(fib$(30));


// When you need the value, just unwrap it with $$( fib$(30) )
console.log( $$( fib$(5) ))


// The only problem that makes this not truly functional, is that
console.log(fib$(5) === fib$(5)) // is false, while it should be true
// but that should be solveable

https://jsfiddle.net/819Lgwtz/42/