函数式编程?柯里化?

289 阅读4分钟

命令式编程中“典型”的方法和过程都深深地根植于它们所在的环境中,通过状态、依赖和有效作用达成;纯函数与此相反,它与环境无关,只要我们愿意,可以在任何地方运行它

面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩...以及整个丛林

一等公民的函数

可作为变量一样被传递、返回或者在函数中嵌套函数、可作为参数、使用总有返回值的表达式而不是语句。

纯函数应该天然,无副作用

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

副作用是指,函数内部与外部互动,产生运算以外的其他结果。 例如在函数调用的过程中,利用并修改到了外部的变量,那么就是一个有副作用的函数。

概括而言,副作用包含:

  • 改变了任何外部变量或对象属性(例如,全局变量,或者一个在父级函数作用域链上的变量)
  • 写日志
  • 在屏幕输出
  • 写文件
  • 发网络请求
  • 触发任何外部进程
  • 调用另一个有副作用的函数

比如 slicesplice,这两个函数的作用并无二致——但是注意,它们各自的方式却大不同,但不管怎么说作用还是一样的。我们说 slice 符合纯函数的定义是因为对相同的输入它保证能返回相同的输出。而 splice 却会嚼烂调用它的那个数组,然后再吐出来;这就会产生可观察到的副作用,即这个数组永久地改变了。

var xs = [1,2,3,4,5];

// 纯的
xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]

xs.slice(0,3);
//=> [1,2,3]


// 不纯的
xs.splice(0,3);
//=> [1,2,3]

xs.splice(0,3);
//=> [4,5]

xs.splice(0,3);
//=> []

在函数式编程中,我们讨厌这种会改变数据的笨函数。我们追求的是那种可靠的,每次都能返回同样结果的函数,而不是像 splice 这样每次调用后都把数据弄得一团糟的函数,这不是我们想要的。

来看看另一个例子。

// 不纯的
var minimum = 21;

var checkAge = function(age) {
  return age >= minimum;
};


// 纯的
var checkAge = function(age) {
  var minimum = 21;
  return age >= minimum;
};

柯里化(curry)

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

function sub_curry(fn) {
    var args = [].slice.call(arguments, 1);
    return function() {
        return fn.apply(this, args.concat([].slice.call(arguments)));
    };
}

function curry(fn, length) {

    length = length || fn.length;
    var slice = Array.prototype.slice;

    return function() {
        if (arguments.length < length) {
            var combined = [fn].concat(slice.call(arguments));
            return curry(sub_curry.apply(this, combined), length - arguments.length);
        } else {
            return fn.apply(this, arguments);
        }
    };
}

// 我们验证下这个函数:
var fn = curry(function(a, b, c) {
    return [a, b, c];
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

上面例子,初次看时,可能会不太明白,这里我们拿fn("a")("b")("c")逐步解析下。

  1. curry传入函数并且赋值给fn
  2. curry函数中length3后返回一个函数。
  3. fn("a")("b")("c") 时,执行上一步骤返回的函数,即curry() { return function() {....}},在这里为了方便,将curry 返回的函数称为fun
  4. fn("a")时,funarguments.length为1,故执行if语句的代码块。
  5. function(a, b, c) {return [a, b, c];}函数作为数组的参数值与funarguments链接( [fn].concat(slice.call(arguments) ) 。
  6. 回调curry,curry的第一个参数为sub_curry函数返回的值,其作用是携带function(a, b, c){...}函数与a值。第二个参数 作用差多少个"参数"。
  7. 重复步骤2以后步骤(但此时length2

当然了,如果你觉得还是无法理解,你可以选择下面这种实现方式,可以实现同样的效果:

function curry(fn, args) {
  let length = fn.length;
  args = args || [];

  return function() {
    let _args = args.slice(0)

    for (let i = 0; i < arguments.length; i++) {
        _args.push(arguments[i])
    }
    if (_args.length < length) {
        return curry.call(this, fn, _args)
    } else {
        return fn.apply(this, _args)
    }
  }
}

var fn = curry(function(a, b, c) {
    console.log([a, b, c])
});

fn("a", "b", "c") // ["a", "b", "c"]
fn("a", "b")("c") // ["a", "b", "c"]
fn("a")("b")("c") // ["a", "b", "c"]
fn("a")("b", "c") // ["a", "b", "c"]

或许大家觉得这种方式更好理解,又能实现一样的效果,为什么不直接就讲这种呢?

因为想给大家介绍各种实现的方法嘛,不能因为难以理解就不给大家介绍呐~

代码组合(compose)

函数饲养

这就是 组合(compose,以下将称之为组合):

var compose = function(f,g) {
  return function(x) {
    return f(g(x));
  };
};

f 和 g 都是函数,x 是在它们之间通过“管道”传输的值。

组合看起来像是在饲养函数。你就是饲养员,选择两个有特点又遭你喜欢的函数,让它们结合,产下一个崭新的函数。组合的用法如下:

var toUpperCase = function(x) { return x.toUpperCase(); };
var exclaim = function(x) { return x + '!'; };
var shout = compose(exclaim, toUpperCase);

shout("send in the clowns");
//=> "SEND IN THE CLOWNS!"

两个函数组合之后返回了一个新函数是完全讲得通的:组合某种类型(本例中是函数)的两个元素本就该生成一个该类型的新元素。把两个乐高积木组合起来绝不可能得到一个林肯积木。所以这是有道理的,我们将在适当的时候探讨这方面的一些底层理论。

在 compose 的定义中,g 将先于 f 执行,因此就创建了一个从右到左的数据流。这样做的可读性远远高于嵌套一大堆的函数调用,如果不用组合,shout 函数将会是这样的:

var shout = function(x){
  return exclaim(toUpperCase(x));
};

我们再看看别的例子

var compose = function (f, g) {
  return function (x) {
    return f(g(x));
  };
}

var add = function (x) {
  let y = x + 1
  console.log('add', y)
  return y
}

var reduce = function (x) {
  let y = x - 1
  console.log('reduce', y)
  return y
}

var  multiplication = function (x) {
  let y = x * 2
  console.log('multiplication', y)
  return y
}

// var fruit = compose(reduce, compose(add,  multiplication)); // 200
var fruit = compose(compose(reduce, add),  multiplication); // 200

fruit(100)

现在是时候去看看所有的组合都有的一个特性了。

// 结合律(associativity)
var associative = compose(f, compose(g, h)) == compose(compose(f, g), h);
// true

这个特性就是结合律,符合结合律意味着不管你是把 g 和 h 分到一组,还是把 f 和 g 分到一组都不重要。


分享不易额,喜欢的话一定别忘了点💖!!!

只关注不点💖的都是耍流氓,只收藏也不点💖的也一样是耍流氓。

参考文献

函数式编程指北

JavaScript-函数式编程