《JS函数式编程指南》这本书值得读很多遍,特别推荐哦~~~
看这本书初衷是看懂ramda,结果发现 函数式编程这个大萝卜。哈哈哈哈。。。
一. 相关术语
- 函数范式(functional paradigm)
- 命令式 (imperative)
- 可变状态(mutable state)
- 无限制副作用(unrestricted side effects)
- 无原则设计(unprincipled design)
- 认知负荷(cognitive load)
- 可缓存性(Cacheable)
- 可移植性/自文档化(Portable/Self-Documenting)
- 类型签名(type signatures)
- 类型约束(type constraints)
- 。。。
我理解的函数式编程就是运用数学中函数
的方式,以通用、可组合的组件形式进行编程,而不是过程化地命令计算机去怎么做。函数式编程优势主要体现在数据不变性
和函数无副作用
两方面;
二. 应避免出现的情况
-
: 用一个函数将另一个函数包装起来, 目的只是延迟执行;
// wrong var getServerStuff = function(callback){ return ajaxCall(function(json){ return callback(json); }); }; // right var getServerStuff = ajaxCall;
因为:
return ajaxCall(function(json){ return callback(json); }); // 等价于 ajaxCall(callback);
第一种书写方式,虽然更易于理解,但是内层函数参数改变时,外层包裹函数也需要同时改变。
- :在命名时将自己限定在特定的数据/情景中;
这是重复造轮子的一大原因;
三. 什么是纯函数
纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用
比如: 数组中的slice 函数则为纯函数,每次应用会得到相同的数据,而splice则不同;
纯函数就是数学上的函数,而且是函数式编程的全部
还有一种的情况就是:在函数中引入了外部的环境,从而增加了认知负荷;
举例:
// 不纯的
var minimum = 21;
var checkAge = function(age) {
return age >= minimum;
};
// 纯的
var checkAge = function(age) {
var minimum = 21;
return age >= minimum;
};
在不纯的版本函数中,其输入值依赖于系统状态。
对于纯函数定义中提到的副作用是指:
副作用是在计算结果的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
只要是和函数外部环境发生的交互就都是副作用;
在纯函数中,并不是要禁止一切副作用,而是让副作用发生在可控的范围内,在纯函数中使用functor和monad进行控制副作用。
四. 纯函数的好处
- 纯函数总能根据输入来做缓存。
实现缓存的一种典型方式是memoize技术。
对于含有类似请求等不纯的函数,通过包裹一层新的函数的延迟执行的方式把不纯的函数变成纯函数。var memoize = function(f) { var cache = {}; return function() { var arg_str = JSON.stringify(arguments); cache[arg_str] = cache[arg_str] || f.apply(f, arguments); return cache[arg_str]; }; }
- 可移植性/自文档化(Portable/Self-Documenting)
纯函数的依赖都在参数中指明,更易于观察理解。
🌰:
在JS中,可移植性意味着把函数序列化并通过socket发送,也可以意味着代码可以在web worker 在运行。// 不纯的 var signUp = function(attrs) { var user = saveUser(attrs); welcomeUser(user); }; // 纯的 var signUp = function(Db, Email, attrs) { return function() { var user = saveUser(Db, attrs); welcomeUser(Email, user); };
命令式编程中的方法和过程都深深的和其运行环境相关,功能通过状态、依赖和有效作用达成。而纯函数正好相反,与环境无关,因此可以移植。 - 可测试性(Testable)
只需要简单的给函数一个输入,然后断言输出就好了。 - 合理性(Reasonable)
如果一段代码可以替换成它所执行的所得的结果,而且是在不改变整个程序行为的前提下替换的,则称这段代码是引用透明的(referential transparency)。
由于纯函数总是相同的输入得到相同的输出,所以纯函数也是引用透明
的。这也是纯函数的很大的一个优点。 - 并行代码
我们可以并行运行任意的纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因为副作用而进入竞争态(race condition);
五. 快速实现纯函数化的工具--柯里化(curry)
-
什么是Curry?
Curry:只传递函数的一部分参数来调用它,让它返回一个函数去处理剩下的参数。
可以一次性地调用curry函数,也可以每次只传一个参数分多次调用。
Ramda 函数本身都是自动柯里化; -
Curry 帮助函数
Lodash、Ramda库中都有Curry 帮助函数。在使用这类函数时有一个很重要的模式就是将要操作的数据放在最后的一个参数中。
🌰 1:const R = require('ramda'); const match = R.curry((what, str) => { return str.match(what); }) match(/\s+/g, 'hhhh'); // or match(/\s+/g)('hhhh');
🌰 2:
// 使用帮助函数 `_keepHighest` 重构 `max` 使之成为 curry 函数 // 无须改动: var _keepHighest = function(x,y){ return x >= y ? x : y; }; // 重构这段代码: var max = function(xs) { return reduce(function(acc, x){ return _keepHighest(acc, x); }, -Infinity, xs); }; var max = R.reduce(_keepHighest, -Infinity);
🌰 3:
// 包裹数组的 slice 函数使之成为 curry 函数 // [1,2,3].slice(0,1); var slice = R.curry(function(start, end, xs){ return xs.slice(start, end); });
六. 代码组合(compose)
- 什么是Compose?
var compose = function(f,g) {
return function(x) {
return f(g(x));
};
};
上面的代码即为代码组合的本质。组合就是将两个或两个以上的函数进行结合返回新的函数。
在组合的定义中,g 将先于f 执行,因此就创建了一个从右到左的数据流。
组合负符合数学中的结合律
compose(a,b,c) === compose(compose(a,b),c) === compose(a,compose(b,c))
结合律的一大好处是任何一个函数分组都可以被拆开来,然后再以它们自己的组合方式打包在一起。
组合中数据的转变如下图:
- Compose有利于实现pointfree
pointfree 模式:函数无须表明要操作的数据的样子。一等公民的函数、curry、compose联合使用有利于实现这种模式。
🌰 :
// 非 pointfree,因为提到了数据:word
var snakeCase = function (word) {
return word.toLowerCase().replace(/\s+/ig, '_');
};
// pointfree
var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);
在pointfree的实现方式中不需要指明操作的数据为word,而在非pointfree的代码中则需要指明。pointfree 代码可以帮我们减少很多不必要的命名。
在pointfree的实现方式中, 是通过管道将数据在接受单个参数的函数间传递。通过Curry,使得compose中的每一个函数都先接受数据,然后操作数据,最后再把结果传递给下一个函数。
- Compose的调试方式
在使用组合时,将多个函数组合在一起,除了最右边的函数可以一次性接收两个及两个以上的参数,其他的函数一次只能接收一个参数,因此经常会出现下面的错误。
// wrong
var latin = compose(map, angry, reverse);
// right
var latin = compose(map(angry), reverse);
调试上面代码的错误可以使用下面这个不纯的trace函数来追踪代码的执行情况:
var trace = R.curry(function(tag, x){
console.log(tag, x);
return x;
});
🌰:
var dasherize = compose(join('-'), toLower, split(' '), replace(/\s{2,}/ig, ' '));
dasherize('The world is a vampire');
//debug
var dasherize = compose(join('-'), toLower, trace("after split"), split(' '), replace(/\s{2,}/ig, ' '));
// after split [ 'The', 'world', 'is', 'a', 'vampire' ]
- Compose使用举例
const _ = require('ramda');
// 数据
const CARS = [
{name: "Ferrari FF", horsepower: 660, dollar_value: 700000, in_stock: true},
{name: "Spyker C12 Zagato", horsepower: 650, dollar_value: 648000, in_stock: false},
{name: "Jaguar XKR-S", horsepower: 550, dollar_value: 132000, in_stock: false},]
// 🌰 1:
// 使用 _.compose()、_.prop() 和 _.head() 获取第一个 car 的 name
// answer
const nameOfFirstCar = _.compose(_.prop('name',_.head));
// 🌰 2:
// 重写下面这个函数
var isLastInStock = function(cars) {
var last_car = _.last(cars);
return _.prop('in_stock', last_car);
};
// answer
const isLastInStock = _.compose(_.prop('in_stock'), _.last);
// 🌰 3:
// 使用_average重写下面函数
var _average = function(xs) { return reduce(add, 0, xs) / xs.length; };
var averageDollarValue = function(cars) {
var dollar_values = map(function(c) { return c.dollar_value; }, cars);
return _average(dollar_values);
};
// answer
const averageDollarValue = _.compose(_average,_.map(_.prop('dollar_value')));
// 🌰 4:
// 重构下面的代码
var availablePrices = function(cars) {
var available_cars = _.filter(_.prop('in_stock'), cars);
return available_cars.map(function(x){
return accounting.formatMoney(x.dollar_value);
}).join(', ');
};
// answer
var formatPrice = _.compose(accounting.formatMoney, _.prop('dollar_value'));
var availablePrices = _.compose(join(', '), _.map(formatPrice), _.filter(_.prop('in_stock')));
// 🌰 5:
// 重构下面的代码
var fastestCar = function(cars) {
var sorted = _.sortBy(function(car){ return car.horsepower }, cars);
var fastest = _.last(sorted);
return fastest.name + ' is the fastest';
};
// answer
var append = _.flip(_.concat);
var fastestCar = _.compose(append(' is the fastest'),
_.prop('name'),
_.last,
_.sortBy(_.prop('horsepower')));
七. 声明式代码
-
什么是声明式代码?
命令式代码是一步步地指示要做怎么做。声明式代码是告诉要做什么,而不是怎么做。虽然命令式代码并不错,但是命令式代码硬编码了一步接一步的执行方式。声明式代码不指定执行顺序,所以更适合于并行执行。
🌰 :// 命令式 var makes = []; for (i = 0; i < cars.length; i++) { makes.push(cars[i].make); } // 声明式 var makes = cars.map(function(car){ return car.make; });
-
可用于重构的等式
// map 的组合律 var law = compose(map(f), map(g)) == map(compose(f, g));
使用上面的等式进行重构可以将两层循环合并成一层循环。
八. Hindley-Milner 类型签名(type signatures)
-
什么是Hindley-Milner 类型签名?
在 Hindley-Milner 系统中,函数都写成类似 a -> b 这个样子,其中 a 和b 是任意类型的变量。🌰 1:
// match :: Regex -> String -> [String] // match :: Regex -> (String -> [String]) var match = curry(function(reg, s){ return s.match(reg); });
对于Hindley-Milner 类型签名:
- 与普通代码一样,类型签名中也使用变量,把变量命名为a 和b 只是一种约定俗成的习惯;
对于相同的变量名,其类型也一定相同。 a -> b 可以是从任意类型的 a 到任意类型的 b,但是 a -> a 必须是同一个类型; - 可以将最后一个类型理解成返回值;
- 将(a -> b)理解成一个类型为a的参数,返回类型为b的结果的函数;
- 签名可以把类型约束为一个特定的接口(interface);这就是类型约束(type constraints)
以上面的规则对reduce进行解释:
🌰 2:
// reduce :: (b -> a -> b) -> b -> [a] -> b
var reduce = curry(function(f, x, xs){
return xs.reduce(f, x);
});
首先reduce接收(b -> a -> b)这样的一个函数作为参数1,函数的两个参数为类型b 和a, b和a
的值来自于 reduce接收的第2个和第3个参数,最终返回类型b 的结果值, 并可以看到结果值类型b和reduce的第一个参数(这个函数)的返回类型相同,则可以看出reduce的返回值则为reduce接收的一个参数的返回值。
🌰 3:
// sort :: Ord a => [a] -> [a]
胖箭头左边指明 a 一定是个 Ord 对象。
- parametricity
一旦引入一个类型变量,就会出现一个奇怪的特性叫parametricity。parametricity 是指此函数将会以一种统一的行为作用于所有的类型。
🌰:
// fun:: [a] -> a
a 告诉我们它不是一个特定的类型,这意味着它可以是任意类型;那么我们的函数对每一个可能的类型的操作都必须保持统一。这就是 parametricity 的含义。
网友评论