前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >[译] 什么是函数式编程

[译] 什么是函数式编程

作者头像
腾讯IVWEB团队
发布2020-06-28 10:41:50
1.5K0
发布2020-06-28 10:41:50
举报

原文: What Is Functional Programming - Ali Spittel 译者按, 函数式编程能够更好地解耦代码, 虽然在前端层次不可能完全避免对函数副作用的需求(界面状态等), 但是诸如将状态与UI组件进行分离, 将UI组件完全的函数化这类的操作能够降低耦合, 并且能够更好地进行代码复用.

作为一名开发者, 自然是想要写出优雅的, 易于维护的, 可扩展的, 可以预测的代码. 函数式编程(Functional Programming / FP)的原则能够很好的命中这些需求.

函数式编程是一种编程范式或者说风格, 在这种范式下开发者更关注不变性, 函数是一等公民, 引用透明性, 以及纯函数性等性质. 如果你还不清楚这些名词那也不用担心, 我们将在这篇文章中逐步了解这些术语.

函数式编程从Lambda计算演变而来, Lambda计算是一种建立在函数抽象与函数推导上的数学系统. 因此, 大部分函数式编程语言看起来都十分的"数学"(译者: 比如Haskell, 实际上JS也满足函数式编程的要求). 好消息是, 并不需要通过专门使用函数式编程语言来引入函数式编程范式. 在这篇文章中, 我们将使用JavaScript来进行演示和示例. JavaScript拥有不少使它能够满足函数式编程要求的同时又不会拘泥于此的特性.

函数式编程的核心原则

既然我们已经讨论了函数式编程是什么, 现在让我们来看看函数式编程背后的核心原则

纯函数 Pure functions

我喜欢将函数比作机器 - 它们接受一组输入(参数), 并且在之后输出一些东西(返回值). 纯函数没有"副作用"或者其他与返回值无关的行为. 一些潜在的副作用包括打印的操作, 比如console.log, 或者对函数外的变量进行操作之类的.

这是一个非纯函数的例子:

代码语言:javascript
复制
let number = 2;

function squreaNumber() {
    number = number * number; // 不纯的操作: 对函数外部的变量进行了修改 
    consol.log(number); // 不纯的操作: 将函数内的操作打印了出来
    return number;
}

相对的, 下面是一个纯函数的例子, 它接受一个输入, 并返回一个输出:

代码语言:javascript
复制
function squreNumber(number) {
    return number * number;
}
squireNumber(2);

纯函数独立于函数外的状态而运行, 它们不应该依赖于任何自身内部以外的变量, 包括全局变量. 在第一个例子中, 我们使用了在函数体外部创建的变量number, 并且在函数体内部对它进行了修改. 这就打破了原则. 如果你深度依赖一个外部的频繁发生变动的变量, 你的代码将会变得既不可预测又难以追踪, 找出bug的位置或者解释变量的值如何变化将会变得更加困难. 相反, 使用只有输入与输出, 并且变量仅存在函数内部的函数, 将会使得调试debug的过程更为简单.

此外, 函数应该遵循引用透明性原则, 这意味着, 对于相同的输入, 函数总会输出相同的输出. 在上述的例子中, 如果对函数传入一个参数2, 那么它将始终返回结果4. 但是对于一个产生随机数的函数来说, 结果就不是这样了. 对于两次调用, 给与相同的输入, 其结果是不同的.

代码语言:javascript
复制
// 非引用透明性的
Math.random();
// 0.1406399143589343
Math.random();
// 0.26768924082159495

不可变性 Immutable

函数式编程同时也重视不可变性, 或者说不会直接修改数据. 不可变性为函数的可预测性提供支持 - 你清楚数据的值, 而且它们也不会被改变, 这将使得代码变得更加简单, 也更容易去测试, 并且也更容易在分布式和多线程应用中被调用.

当开始处理数据结构时, 不可变性会频繁地受到影响. 例如许多JavaScript中的数组方法都会直接地改变数组本身. 比如.pop()会直接移除数组的最后一个元素, .splice()会将数组中的一部分移除. 而在函数式范式中, 我们会从原数组中复制一个新数组出来, 并在这个过程中移除我们想要移除的元素

代码语言:javascript
复制
// 直接改变 myArr
const myArr = [1, 2, 3];
myArr.pop(); // 3
console.log(myArr); // [1, 2];
代码语言:javascript
复制
// 复制原数组, 并且不带上最后一个元素
const myArr = [1, 2, 3];
const myNewArr = myArr.slice(0, 2); // [1, 2]
console.log(myArr); // [1, 2, 3]

函数是一等公民 First-class functions

在函数式编程中, 函数是一等公民, 这意味着他们能够被像其他的变量那样作为进行使用. 我们能够创建一个函数的数组, 或者将函数作为参数传递给其他函数, 或者将他们保存在变量中.

代码语言:javascript
复制
const myFunctionArr = [() => 1 + 2, () => console.log('hi'), x => 3 * x];
myFunctionArr[2](2); // 6

const myFunction = anotherFunction => anotherFunction(20);
const secondFunction = x => x * 10;
myFunction(secondFunction); // 200

高阶函数 Higher-order functions

高阶函数是指完成这两个任务之一的函数: 使用一个或多个函数作为他的参数; 返回一个函数. JavaScript内建了许多第一类的高阶函数, 比如在数组中常用的filter, map, reduce.

filter用来从原数组中, 对元素筛选满足条件的部分后保持顺序返回新的数组

代码语言:javascript
复制
const myArr = [1,2,3,4,5];

const evens = myArr.filter(x => x % 2 === 0); // [2, 4]

map用来遍历整个数组, 并且对每个元素根据传入的逻辑进行一个映射. 在下面这个例子中, 我们通过给map函数传入一个函数来将每个元素都乘以2

代码语言:javascript
复制
const myArr = [1, 2, 3, 4, 5];

const doubled = myArr.map(i => i * 2); // [2, 4, 6, 8, 10]

reduce根据输入的数组输出一个单一的值, 通常用来计算数组的元素的值的总和, 或者扁平化数组, 或者将元素分组.

代码语言:javascript
复制
const myArr = [1, 2, 3, 4, 5];

const sum = myArr.reduce((i, runningSum) => i + runningSum); // 15

建议各位读者自己实现一次每个方法! 举个例子, 可以像这样创建一个filter函数:

代码语言:javascript
复制
const filter = (arr, condition) => {
    const filteredArr = [];

    for (let i = 0; i < arr.length; i++) {
        if (condition(arr[i])) {
            filteredArr.push(arr[i]);
        }
    }

    return filteredArr;
}

第二类高阶函返回一个函数作为其返回值, 也是一个相对常见的范式. 举个例子:

代码语言:javascript
复制
const createGreeting = greeting => persion => `${greeting} ${person}`;

const sayHi = createGreeting('Hi');
console.log(sayHi('Ali')); // 'Hi Ali'

const sayHello = createGreeting('Hello');
console.log(sayHello('Ali')); // 'Hello Ali'

顺带一提, 函数的柯里化(Currying)是一个很类似的技术, 有兴趣的话可以看看这里

函数组合 Function composition

将多个简单函数按照一定顺序组合成为一个复杂函数的过程被称为函数组合. 例如可以将averagesum两个函数组合起来变成一个averageArray的函数用来Number数组的平均值. 每一个独立的function都相对较小, 并且可以被复用于其他目的, 而组合后的它们能完成更加完整而独立的任务:

代码语言:javascript
复制
const sum = arr => arr.reduce((i, runningSum) => i + runningSum);
const average = (sum, count) => sum/count;

const averageArray = arr => average(sum(arr), arr.length);

函数式编程的好处

函数式编程使得代码更加的模块化. 开发者可以使用体量更小的, 可以被一次又一次复用的函数. 了解每一个函数的功能与特性意味着能够更清晰明了地进行调试与测试. 更不用说这些函数都是可预测的.

此外, 对于多核的开发, 可以放心地向这些CPU核心分发函数的运行(译者: 因为只关心输入和输出了, 不会受到外部变量或者状态的影响), 继而能够达到更高的运行效率.

怎么样才能使用函数式编程?

开发者不需要完全地遵守每一个函数式编程的规定. 尽管面向对象编程通常被视作与函数式编程相违背的对手, 但开发者仍然可以在使用函数式编程的一些原则和特性的时候结合面向对象的编程范式来进行开发.

举例来说, React, 吸收了很多函数编程的原则, 例如不可变的state, 但同时多年来也保留了基于类的语法.

函数式编程几乎可以通过任何一个编程语言来实现, 并不需要开发者去写Clojure或者Haskell(除非你真的想).

即使函数式原则遵循得并不纯粹, 函数式编程仍然能给你的代码带来不小的好处.

译者按, 这里推荐一个通用的JavaScript的函数式编程基础库. 另外需要注意, 这篇文章只是一个入门的介绍, 真正要系统学习函数式的话, 需要去了解离散数学相关的函子这一概念之类之类的.

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 函数式编程的核心原则
    • 纯函数 Pure functions
      • 不可变性 Immutable
        • 函数是一等公民 First-class functions
          • 高阶函数 Higher-order functions
            • 函数组合 Function composition
            • 函数式编程的好处
            • 怎么样才能使用函数式编程?
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档