Functional Programming

2015-10-16 Jason Liao 更多博文 » 博客 » GitHub »

原文链接 http://jasonliao.me/posts/2015-10-16-functional-programming.html
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。


Table of Content

First Class Functions

当我们说函数是第一类对象的时候,并不意味着函数有多特别,相反,我们认为函数并不特别,你可以把它当成任一类型地进行存储,传参,或者赋值

没错,我们都知道,但是却经常可以 看到这些多此一举的代码

var hi = function (name) {
  return 'Hi' + name;
};

var greeting = function (name) {
  return hi(name);
};

因为函数是可以赋值的,所以我们可以直接把 hi 赋给 greeting

var greeting = hi;

再来看看一个例子(代码摘自npm)

var getServerStuff = function (callback) {
  return ajaxCall(function (json) {
    return callback(json);
  });
};

// 以上代码等价于
var getServerStuff = ajaxCall;

一开始我理解不了,为什么会等价,后来搞清楚后才真正发现,这样写代码不仅仅增加代码量,还对日后的维护困难度和代码的可读性都大大降低

这里解释一下为什么等价

// 先看看 ajaxCall 里的那个匿名函数
function (json) {
  return callback(json);
}

// 我们为这个匿名函数加个名字为 noName,即
var noName = function (json) {
  return callback(json);
}

// 当我们调用这个 noName 函数的时候,返回的是 callback(json),即
noName(json); // callback(json);

// 所以这个匿名函数 noNane 其实就等于 callback
// 那么
return ajaxCall(function (json) {
  return callback(json);
});

// 就相等于
return ajaxCall(callback);

// 同样的道理
var getServerStuff = function (callback) {
  return ajaxCall(callback);
};

// 调用 getServerStuff 函数的时候
// 返回的是 ajaxCall(callback),即
getServerStuff(callback); // ajaxCall(callback)

// 所以
var getServerStuff = ajaxCall;

另外,如果一个函数不必要的被包裹了起来,当它要发生改动的时候,那么包裹它那个函数也要做出相应的变化

httpGet('/post/2', function (json) {
  return renderPost(json);
}

但突然发现 httpGet 要改成可以抛出一个可能出现的 err 异常,那这时我们就要把两个函数都改写

httpGet('/post/2', function (json, err) {
  return renderPost(json, err);
}

但如果我们直接把 renderPost 函数当成参数传进去的时候,那就只要改 renderPost 函数就可以了

httpGet('/post/2', renderPost);

Pure Functions

A pure function is a function that, given the same input, will always return the same output and does not have any observable side effect.

所谓的纯函数就是对相同的输入,它总是能返回相同的输出,简单的说,就是执行了纯函数之后,不会对原来的数据有影响

再看一个例子

//  impure
var minimum = 21;

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

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

在不纯的版本中,checkAge 的结果取决于 minimum 这个可变的变量,也就是说,它的值取决于系统的状态,当我们不知道在哪里修改这个变量的值的时候,checkAge 的返回值也会跟着改变

What can pure functions do

  • Cacheable

一个纯函数总能根据输入来做缓存,这样不仅可以使同样的输入总是返回同样的东西,然后还可以提高运行的速率

我们可以定义一个 memoize 函数,可以给它传入任意一个函数,不管这个函数有多大的破坏性

  var memoize = function (fn) {
    var cache = {};

    return function () {
      var arg_str = JSON.stringify(arguments);
      cache[arg_str] = cache[arg_str] || fn.apple(fn, arguments);
      return cache[arg_str];
    };
  };

  // 定义一个 squareArea 函数
  var squareArea = memoize(function (x) { return x * x; });

  squareArea(5);  // 25
  squareArea(5);  // 25 : form cache
  • Testable

纯函数非常容易测试,你输入的是什么,输出就是什么,不会受环境的影响,所以不需要在测试前配置和模拟一堆的环境

  • Parallel Code

因为纯函数不会不会受外围环境还有变量的影响,所有不需要访问共享的内存,而且纯函数也不会因副作用而进入竞争态(race condition)

代码并行在服务器端的 JavaScript 和使用了 Web worker 的浏览器是非常容易实现的,但对于现阶段来说,出于 Web worker 的兼容性还有非纯函数的复杂性来考虑,还是避免使用代码并行为好

Curry

柯里化的概念很简单,只传递给一个函数一部分的参数来调用它,让它返回一个函数去处理剩下的参数

下面来看一个例子

// 首先定义一个处理多参数的函数 
// 例如一个多参数求和 add

function add () {
  var sum = 0,
      i = 0;
  for (; i < arguments.length; i++) {
      sum += arguments[i];
  }
}

// 这时对 add 函数进行 柯里化
var currilyAdd = curry(add);

// 可以先处理一部分参数,来得到一个函数
var add10 = currilyAdd(10);

// 得到了一个 add10 函数,更直观,可读
console.log(add10(11)); // 21

curry 是怎么实现的呢

function curry (fn) {
  // 返回 柯里化 后得到的函数
  return function () {
    // 会把第一次调用 柯里化后函数 的参数存起来
    var args = [].slice.apply(arguments);
    return function () {
        // 第二次调用的时候,把之前保存起来的参数
        // 一同调用原来的功能函数
        return fn.apply(fn, args.concat([].slice.apply(arguments)));
    };
  };
}

柯里化,运用的就是函数式编程的思想

它把已有的函数柯里化后,与参数结合得到一个新函数,使得这个新函数功能更加的清晰