vlambda博客
学习文章列表

函数式编程(一):编程范式之间的比较


本文是“函数式编程”系列文章的第一篇文章。

在这篇文章中,我们将讨论编程范式。涉及命令式编程和声明式编程。例举一些简单的例子进行比较。最后,我们将从进化的角度来做一些总结。

1 编程范式

image.png

编程范式是编程的一种风格。一些语言迫使我们以某种范式编程。另一些语言让程序员自己选择。每个范例都遵循一组概念。在计算机编程的历史上,工程师们开发了不同的语言。每种语言都有一个(或更多)范式。这些范式属于以下两个主要类别之一:

1.1 命令式范式

这种范式中,控制流是显式的,程序员指示程序如何更改其状态。包括一下子范例:

  • 结构范式
  • 面向对象范式

1.2 声明式范式

这种范式中,控制流是隐式的,程序员指示程序应该做什么,而不指定应该如何做。包括以下子范式:

  • 函数式范式
  • 逻辑范式
  • 数学范式
  • Reactive范式

1.3 总结

大多数语言要么属于命令式范式,要么属于声明式范式,其中每种范式都有一套遵循的规则。

2 命令式范式

相比结构范式,命令式范式在结构方面有了一些进步,但它仍然存在一些问题:

  • 告诉程序如何做事情(控制流是显式的)
  • 共享状态

2.1 问题1:告诉程序如何做事情(控制流是明确的)

案例:想象一下,一位领导带领1000名员工完成一项工程。领导开始一个接一个地告诉1000名员工如何做事。你觉得会有多糟糕,你肯定可以看到这种微观层面的管理方式存在重大风险和陷阱,甚至根本无法执行。
解决方案:在职责范围内对人员进行分组,并向每个小组委派一名组长。每个小组的组长都应该知道如何处理任务。这将大大降低复杂性和瓶颈,并且将更容易管理。在这个类比中:

  • 任务负责人=程序员。
  • 团队领导=高级别的功能
  • 每组员工=代码行 结论:当我们在项目层面应用高阶组织结构时,我们的生活将更加顺畅。

2.2 问题2:共享状态

案例:想象你自己是一个父亲,你有两个孩子。你为孩子们创建了一个共享银行账户。每个月你在那个账户里存1000美元。您的两个孩子都不知道该帐户是共享的。所以他们都认为自己有1000美元可以花。到了月底,你尴尬了,因为你发现你最后在那个账户里欠了1000美元。解决方案:每个孩子应该有单独的帐户和指定的每月金额 在这个类比中:

  • 儿童=功能
  • 共享银行账户=共享状态。结论:当你的功能共享相同的状态时。他们无意识地使用它。即使只有两个函数,这也会扰乱程序状态。因此,最好每个函数都有自己的独立状态。

2.3 命令式编程的例子

const sum = (list) => {
    let result = 0
    for (let i = 0; i < list.length; i++){
        result += list[i]
    }
    return result
}

为什么这被认为是命令式编程?

  • 告诉程序如何做事情:我们显式地告诉for循环如何工作。此外,我们还显式地访问数组中的每个元素。
  • 共享状态:结果变量是一个共享状态,在每次迭代中都会发生变化(在大的任务中共享状态将更麻烦)。

3 声明式范式

声明式范式是程序员在不指定如何操作的情况下指示程序应该做什么。与命令式范式相比,陈述式范式只是不具备命令性。换句话说,在声明式范例中,我们编写的函数:描述一个程序应该执行什么而不是如何执行(隐式控制流)。不会产生副作用(我们将在后面详细讨论)。

3.1声明式编程的例子

我们看到了sum函数是如何在命令式范式中实现的,让我们看看它是如何以声明式范式实现的。

const add = (a, b) => a + b
const sum = (list) => list.reduce(add)

很神奇,对吧?但为什么这被认为是声明性的呢?

  • 描述了程序应该执行什么而不是如何执行(隐式控制流):没有显式迭代器,没有显式地告诉循环如何工作或如何访问元素。这是通过使用reduce实现的。
  • 没有产生副作用:共享状态是一种副作用形式,使用reduce和add函数的组合完全消除了这种副作用。

4 更进一步的例子

如果我们只想求偶数的和呢:命令式编程解决方案

const evenSum = (list) => {
    let result = 0
    for (let i = 0; i < list.length; i++){
        if(list[i] % 2 === 0) {
            result += list[i]
        }
    }
    return result
}

声明式编程解决方案

const evenSum = (list) => {    
    const isEven = (n) => n % 2
    const add = (a, b) => a + b
    return list.filter(isEven).reduce(add)
}

正如我们所看到的,如果我们想比较两种范式(命令式和声明式),声明性范式中更像齿轮, 你把齿轮作为一个独立的单元来开发,然后在需要的地方添加它们。但在命令式范式中,它更像是面团, 几乎所有东西都混合并融合到同一段代码中(除非你在设计模式方面做得很好)。
总的来说,陈述式范式更适合:

  • 可预测的
  • 可测试
  • 可重复使用的
  • 可定制
  • 可缓存
  • 可维护
  • 组合的 其中一些观点在sum示例中不一定有体现,但在以后的文章中会有体现。

5 进化的视角

理解了两种主要范式——命令式和声明式,而且每种范式都有子范式。现在让我们从进化的角度进一步讨论结构、面向对象和函数数式范式。每种范式都通过引入新的东西来限制编程方式:

  • 结构范式:通过在我们的代码中引入if/else/then/loop等结构,限制goto和‘控制转移流’的使用。换句话说,它限制了控制权的转移。
  • 面向对象范例:通过继承引入多态性。
  • 函数式范式:通过引入不变性来限制共享状态和副作用。

请记住,每个范式可能使用一个或多个其他范式的概念(例如,面向对象范式和函数式范式都使用结构范式概念)。

6  总结

在现实工作中,我们有不同的范式和不同的风格。运用更多的范式会让我们变的更强,在这些范式中变得越强大,解决方案就越强大。
待续...

译自:https://blog.bitsrc.io/functional-programming-part-0-a-brief-comparison-between-programming-paradigms-3ff192cd32b6

                                                         彦祖,点个关注和在看呗!