函数式编程

什么是函数式编程

高阶函数

闭包

纯函数

柯里化

函数组合

函子

函数式编程中非常重要的概念——纯函数(Pure Function)

简单来说,一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。简单来说,只有两点:

  • 函数的返回结果只依赖于它的参数
  • 函数的执行过程里面没有副作用

函数的返回结果只依赖于它的参数

const a = 1;
const foo = (b) => a + b;
foo(2); // 3

foo 函数不是一个纯函数,因为它返回的结果依赖于外部变量a,我们在不知道 a 的值的情况下,并不能保证 foo(2) 的返回值是 3。返回值是不可预测的

const a = 1;
const foo = (x, b) => x + b;
foo(1, 2); // 3

现在的 foo 的返回值只依赖于它的参数 xbfoo(1, 2) 永远是 3。只要 foo 代码不变,你传入的参数是确定的,那么 foo(1, 2) 的值永远是可预测的。

这就是纯函数的第一个条件:一个函数的返回结果只依赖于它的参数

函数执行过程没有副作用

一个函数执行过程对产生了外部可观察的变化,呢么就说这个函数有副作用

我们修改一下foo

const a = 1;
const foo = (obj, b) => {
    return obj.x + b;
};
const counter = { x: 1 };
foo(count, 2); // 3
counter.x; // 1

我们把原来的 x 换成了 obj,我现在可以往里面传一个对象进行计算,计算过程不会对传入的对象进行修改,计算前后的 counter 不会发生任何变化,计算前是 1,计算后也是 1,它现在是纯的,但我们再稍微修改一下:

const a = 1;
const foo = (obj, b) => {
    obj.x = 2;
    return obj.x + b;
};
const counter = { x: 1 };
foo(counter, 2); // 4
counter.x; // 2

我们在 foo 内部加了一句 obj.x = 2,计算前的 counter.x 是 1,但是计算以后 counter.x 是 2。foo 函数的执行对外部的 counter 产生了影响,他又副作用,因为它修改了外部传进来的对象,它不纯了。

要想看懂一些牛逼的源码

必须掌握的编程思维就是 函数式编程

const compose = (f, g) => x => f(g(x))

const f = x => x + 1;
const g = x => x * 2'
const fg = compose(f, g)
fg(1)

纯函数的好处

纯函数的概念

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用

副作用的概念

副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互

副作用可能包括,但不限于:

  • 更改文件系统
  • 往数据库插入记录
  • 发送一个 http 请求
  • 可变数据
  • 打印 log
  • 获取用户输入
  • DOM 查询

追求“纯”的理由

可缓存性(Cacheable)

可测试性(Testable)

合理性(Reasonabel)

柯里化(Curry)

curry 的概念

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数

偏函数的简单应用

Function.prototype.bind()

会创建一个新函数(称为绑定函数),新函数与被调函数(绑定函数的目标函数)具有相同的函数体

https://github.com/sunyongjian/FP-Code/tree/master/srcopen in new window

https://juejin.cn/post/6940442700889980965open in new window

概念

函数式编程倡导利用若干简单的执行单元让计算结果不断渐进,逐层推导复杂的运算

https://juejin.cn/post/6940442700889980965open in new window

函数式编程有两个最基本的运算: 合成(compose)和柯里化(Currying)

合成(Compose)

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做“函数的合成”(compose)

合成的好处显而易见,它让代码变得简单而富有可读性,同时通过不同的组合方式,我们可以轻易组合出其他常用函数,让我们的代码更具表现力

function f1(arg) {
    console.log('f1', arg);
    return arg;
}

function f2(arg) {
    console.log('f2', arg);
    return arg;
}

function f3(arg) {
    console.log('f3', arg);
    return arg;
}

function compose(...funcs) {
    if (funcs.length === 0) {
        return (arg) => arg;
    }
    if (funcs.length === 1) {
        return funcs[0];
    }
    // funcs 是个数组,表示参数的集合,所以能使用 reduce
    // reduce 中 (累加值, 当前值)
    return funcs.reduce(
        (a, b) =>
            (...args) =>
                a(b(...args)),
    );
}

let res = compose(f1, f2, f3)('omg'); // f1(f2(f3("omg")))

console.log('res', res); // f

柯里化(Currying)

概念:把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

所谓“柯里化”,就是把一个多参数的函数,转化为单参数函数

// 柯里化之前
function add(x, y) {
    return x + y;
}

add(1, 2); // 3

// 柯里化之后
function add(y) {
    return function (x) {
        return x + y;
    };
}
// 箭头函数表示
const add = (x) => (y) => x + y;

add(2)(1);

这样调用上述函数: (add(3))(4) 或直接 add(3)(4)

多个箭头函数是什么意义

const addSumArrow = (a) => (b) => a + b;
// 等于
const addSumNormal = function (a) {
    return function (b) {
        return a + b;
    };
};
function enhancer(originF) {
    return function (...args) {
        console.log('before');
        const result = originF(...args);
        console.log('after');
        return result;
    };
}
// 等于
const enhancer =
    (originF) =>
    (...args) => {
        console.log('before');
        const result = originF(...args);
        console.log('after');
        return result;
    };

两数之加

add = (a, b) => a + b;
c_add = (a) => (b) => a + b;

三数之乘

multi = (a, b, c) => a * b * c;
c_multi = (a) => (b) => (c) => a * b * c;

单参定理

「多参数函数」一定能改写为「单参数函数」,且其输入输出保持不变

「赋值」会导致数据共享变得麻烦,你无法信任别人不改你的数据,别人也无法信任你:

  • 要么就靠大家自觉,约定大家都不改 options(我只能说祝你好运);
  • 要么传给别人之前先深拷贝一下,拿到别人的数据之后最好也先深拷贝一下……

数据不可变的两个有点:

  • 数据可变将导致[代入法]不可用,函数与数学再无关联;而数据不可变则相反
  • 数据可变将导致数据共享变得困难;而数据不可变则相反

函数式编程无副作用是因为它不会修改自己外部的 env,也不会去修改通过参数传进来的对象,顶多修改自己的本地变量而已

函数作为一等对象最大的好处就是可以在程序运行时创建它们并将之储存在变量里。如下

function add(a, b) {
    return a + b;
}

add(5, 2);
add(5, 2);
add(5, 100)

这里我们可以优化 add 函数,每次使用 add 函数都是将数字 5 和其他三个数字进行相加,如果能把数字 5 内置在函数中而不用调用时作出参数传进去是个不错的注意。我们可以用柯里化(partial application 或者 currying)

var add = function (a, b) {
    return a + b;
};
function add5(b) {
    return add(5, b);
}
add5(2);
add5(5);
add5(200);

现在,我们创建了一个调用 add 函数并预置了一个参数值(这里是 5)的 add5 函数, add5 函数本质上来讲其实就是预置了一个参数(柯里化)的 add 函数。不过,这个例子并没展示出这门技术动态的一面,如果我们提供的默认值是另外一个应该这么做?按照上面的例子,我们必须再次新建一个函数来提供一个新的预置参数。

function add(a, b) {
    return a + b;
}
function curryAdd(a) {
    return function (b) {
        return add(a, b);
    };
}
var add5 = curryAdd(5);

add5(2);
add5(5);
add5(200);

现在来介绍一下这个新的函数 curryAdd,它接收一个参数,这个参数会作为 add 函数的参数 a,同时返回一个新的匿名函数,这个匿名函数接收一个参数 b 来作为 add 函数的另一个参数。

当我们通过 curryAdd(5) 来调用这个函数时,它返回一个已经储存了我们一个明确参数值的函数,这个参数值此时被当做时这个匿名函数的一个局部变量。因为我们创建了一个闭包,所以即使这个匿名函数已经执行完毕,但我们还是可以通过它来最终求出我们需要的 a + b 的值

const init = (platform) => ({
    host: 'http://www.baidu.com',
    payload: null,
    isSpa: true
}) => {
    console.log('1111')
}

手写柯里化

柯里化就是一种将使用多个参数的一个函数转换为一系列使用一个参数的函数的技术

通俗来讲:用闭包把参数保存起来,当参数的数量足够执行函数了,就开始执行函数

实现

  • 判断当前函数传入的参数是否大于或等于 fn 需要参数的数量,如果是,直接执行 fn
  • 如果传入参数数量不够,返回一个闭包,暂存传入的参数,并重新返回 currying 函数
function currying(fn, ...args) {
    if (args.length >= fn.length) {
        return fn(...args);
    } else {
        return (...args2) => currying(fn, ...args, ...args2);
    }
}

我们来一个简单的实例验证一下:

function fun(a, b, c) {
    return a + b + c;
}
const curryingFun = currying(fun);
curryingFun(1)(2)(3); // 1 2 3
curryingFun(1, 2)(3); // 1 2 3
curryingFun(1, 2, 3); // 1 2 3

参考资料

Last Updated:
Contributors: johan