JS函数式编程指南笔记1-4章

指南地址

1 一个简单的例子

var add = function(x, y) { return x + y };
var multiply = function(x, y) { return x * y };
var flock_a = 4;
var flock_b = 2;
var flock_c = 0;
var result = add(multiply(flock_b, add(flock_a, flock_c)), multi
ply(flock_a, flock_b));
//=>16
// 结合律(assosiative)
add(add(x, y), z) == add(x, add(y, z));
// 交换律(commutative)
add(x, y) == add(y, x);
// 同一律(identity)
add(x, 0) == x;
// 分配律(distributive)
multiply(x, add(y,z)) == add(multiply(x, y), multiply(x, z));
// 原有代码
add(multiply(flock_b, add(flock_a, flock_c)), multiply(flock_a,
flock_b));
// 应用同一律,去掉多余的加法操作(add(flock_a, flock_c) == flock_a)
add(multiply(flock_b, flock_a), multiply(flock_a, flock_b));
// 再应用分配律
multiply(flock_b, add(flock_a, flock_a));

函数式编程就是如上的例子一样可以用数学的思维

2 一等公民函数

var hi = function(name){
    return "Hi " + name;
};
var greeting = function(name) {
    return hi(name);
};
greeting("times");

这里 greeting 指向的那个把 hi 包了一层的包裹函数完全是多余的。可以直接如下写法:

var greeting = hi;
greeting("times");
// "Hi times"

类似的例子:

// 太傻了
var getServerStuff = function(callback){
    return ajaxCall(function(json){
        return callback(json);
    });
};
// 这才像样
var getServerStuff = ajaxCall;

为什么那样写不好?

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

如果 httpGet 要改成可以抛出一个可能出现的 err 异常,那我们还要回过头去把“胶水”函数也改了。


// 把整个应用里的所有 httpGet 调用都改成这样,可以传递 err 参数。
httpGet('/post/2', function(json, err){
    return renderPost(json, err);
});
写成一等公民函数的形式,要做的改动将会少得多:
httpGet('/post/2', renderPost); // renderPost 将会在 httpGet 中调用,想要多少参数都行

this 就像一块脏尿布,我尽可能地避免使用它,因为在函数式编程中根本用不到它。然而,在使用其他的类库时,你却不得不向这个疯狂的世界低头。

3 纯函数的好处

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

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;
};
  • 在不纯的版本中, checkAge 的结果将取决于 minimum 这个可变变量的值。换句话说,它取决于系统状态(system state);这一点令人沮丧,因为它引入了外部的环境,从而增加了认知负荷(cognitive load)。
  • 另一方面,使用纯函数的形式,函数就能做到自给自足。我们也可以让 minimum成为一个不可变(immutable)对象,这样就能保留纯粹性,因为状态不会有变化。要实现这个效果,必须得创建一个对象,然后调用 Object.freeze方法:
var immutableState = Object.freeze({
    minimum: 21
});

3.1 副作用都有什么?

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

副作用包括但不限于:

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

函数关系就是一个输入对应一个输出,和小学数学是一样的

3.2 追求纯函数的理由?

3.2.1 可缓存性(Cacheable):

比如memoize技术

var squareNumber = memoize(function(x){ return x*x; });
squareNumber(4);
//=> 16
squareNumber(4); // 从缓存中读取输入值为 4 的结果
//=> 16
squareNumber(5);
//=> 25
squareNumber(5); // 从缓存中读取输入值为 5 的结果
//=> 25
3.2.2 可移植性/自文档化(Portable/Self-Documenting)

不纯的函数总会有些偷偷摸摸的小动作

// 不纯的
var signUp = function(attrs) {
    var user = saveUser(attrs);
    welcomeUser(user);
};
var saveUser = function(attrs) {
    var user = Db.save(attrs);
    ...
};
var welcomeUser = function(user) {
    Email(user, ...);
    ...
};
// 纯的
var signUp = function(Db, Email, attrs) {
    return function() {
        var user = saveUser(Db, attrs);
        welcomeUser(Email, user);
    };
};
var saveUser = function(Db, attrs) {
    ...
};
var welcomeUser = function(Email, user) {
    ...
};
  • 命令式编程中“典型”的方法和过程都深深地根植于它们所在的环境中,通过状态、依赖和有效作用(available effects)达成;纯函数与此相反,它与环境无关,只要我们愿意,可以在任何地方运行它。
  • 你上一次把某个类方法拷贝到新的应用中是什么时候?我最喜欢的名言之一是Erlang 语言的作者 Joe Armstrong 说的这句话:“面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩…以及整个丛林”。
3.2.3 可测试性(Testable)
  • 第三点,纯函数让测试更加容易。我们不需要伪造一个“真实的”支付网关,或者每一次测试之前都要配置、之后都要断言状态(assert the state)。只需简单地给函数一个输入,然后断言输出就好了。
  • 事实上,我们发现函数式编程的社区正在开创一些新的测试工具,能够帮助我们自动生成输入并断言输出。这超出了本书范围,但是我强烈推荐你去试试Quickcheck——一个为函数式环境量身定制的测试工具。
3.2.4 合理性(Reasonable)

很多人相信使用纯函数最大的好处是引用透明性(referential transparency)。如果一段代码可以替换成它执行所得的结果,而且是在不改变整个程序行为的前提下替换的,那么我们就说这段代码是引用透明的。

var Immutable = require('immutable');
var decrementHP = function(player) {
    return player.set("hp", player.hp-1);
};
var isSameTeam = function(player1, player2) {
    return player1.team === player2.team;
};
var punch = function(player, target) {
    if(isSameTeam(player, target)) {
        return target;
    } else {
        return decrementHP(target);
    }
};
var jobe = Immutable.Map({name:"Jobe", hp:20, team: "red"});
var michael = Immutable.Map({name:"Michael", hp:20, team:"green"});
punch(jobe, michael);
//=> Immutable.Map({name:"Michael", hp:19, team: "green"})
3.2.5 并行代码

最后一点,也是决定性的一点:我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

4 柯里化(curry)

我父亲以前跟我说过,有些事物在你得到之前是无足轻重的,得到之后就不可或缺了。微波炉是这样,智能手机是这样,互联网也是这样——老人们在没有互联网的时候过得也很充实。对我来说,函数的柯里化(curry)也是这样。

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

你可以一次性地调用 curry 函数,也可以每次只传一个参数分多次调用。

var add = function(x) {
    return function(y) {
        return x + y;
    };
};

var increment = add(1);
var addTen = add(10);

increment(2);
// 3
addTen(2);
// 12

下面有一些新的curry函数,具体讲解一下:

var curry = require('lodash').curry;
var match = curry(function(what, str) {
    return str.match(what);
});
var replace = curry(function(what, replacement, str) {
    return str.replace(what, replacement);
});
var filter = curry(function(f, ary) {
    return ary.filter(f);
});
var map = curry(function(f, ary) {
    return ary.map(f);
});

我在上面的代码中遵循的是一种简单,同时也非常重要的模式。即策略性地把要操作的数据(String, Array)放到最后一个参数里。到使用它们的时候你就明白这样做的原因是什么了。

match(/\s+/g, "hello world");
// [ ' ' ]
match(/\s+/g)("hello world");
// [ ' ' ]
var hasSpaces = match(/\s+/g);
// function(x) { return x.match(/\s+/g) }
hasSpaces("hello world");
// [ ' ' ]
hasSpaces("spaceless");
// null
filter(hasSpaces, ["tori_spelling", "tori amos"]);
// ["tori amos"]
var findSpaces = filter(hasSpaces);
// function(xs) { return xs.filter(function(x) { return x.match(
/\s+/g) }) }
findSpaces(["tori_spelling", "tori amos"]);
// ["tori amos"]
var noVowels = replace(/[aeiou]/ig);
// function(replacement, x) { return x.replace(/[aeiou]/ig, repl
acement) }
var censored = noVowels("*");
// function(x) { return x.replace(/[aeiou]/ig, "*") }
censored("Chocolate Rain");
// 'Ch*c*l*t* R**n'

这里表明的是一种“预加载”函数的能力,通过传递一到两个参数调用函数,就能得到一个记住了这些参数的新函数。

当我们谈论纯函数的时候,我们说它们接受一个输入返回一个输出。curry 函数所做的正是这样:每传递一个参数调用函数,就返回一个新函数处理剩余的参数。这就是一个输入对应一个输出啊。

哪怕输出是另一个函数,它也是纯函数。当然 curry 函数也允许一次传递多个参数,但这只是出于减少 () 的方便。

你可能感兴趣的:(函数式编程)