纯函数
相同的输入永远会得到形同的输出。而且没有任何副作用,也不依赖外部环境的状态。
例如:Array.slice() 就是一个纯函数,他没有任何副作用,相同的输入会有相同的输出。
通过代码来看:
var arr = [1, 2, 3, 4, 5];
arr.slice(0, 3); // => [1, 2, 3, 4]
arr.slice(0, 3); // => [1, 2, 3, 4]
arr.slice(0, 3); // => [1, 2, 3, 4]
arr.slice(0, 3); // => [1, 2, 3, 4]
首先它每次对 arr 操作的时候不会改变原先的 arr,不修改状态。它也不依赖于外部的任何东西。那么它也没有副作用,最后它每次相同的输入得到的值都是一样的。这就是一个纯函数。
优点
通过代码来看:
const partial = (f, ...args) => (...moreArgs) => f(...args, ...moreArgs);
接收一个函数 f 和 其他参数,返回一个函数,可以接收更多参数。然后再返回 f 函数的执行结果。把开始的参数和接收到的更多参数,都放在 f 里面执行。
柯里化
柯里化就是偏应用函数的的具体实现。它是把一个多参数的偏应用函数转为一个嵌套一元函数的过程。传递一个参数返回一个函数去处理剩下的参数。
通过代码来看:
const checkage = min => (age => age > min);
const checkage18 = checkage(18);
checkage18(20);
// 也可以这样写
checkage(18)(20);
首先传递一个参数 min —>18 然后返回一个函数, 返回的参数是可以拿到父层的作用域中的 min 的,然后这个返回的函数也接收一个参数 age,那么就可以做到每次只传递一个参数。
反柯里化
函数柯里化是固定部分参数,返回一个接受剩余参数的函数,也称为部分计算函数,目的是为了缩小使用范围,创建一个针对性更强的函数。
反柯里化,意义和用法跟柯里化正好相反,扩大使用范围,创建一个应用范围更广的函数。使本来只有特定对象只适用的方法,扩展到更多的对象。
通过代码来看:
Function.prototype.unCurring = function(){
var self = this;
return function(){
var obj = Array.prototype.shift.call(arguments);
return self.apply(obj, arguments);
}
}
var push = Array.prototype.push.unCurring(),
obj = {};
push(obj, "first", "two");
console.log(obj);
反柯里化就是为了更多的去一次性接收数组。扩大使用范围。
函数的柯里化
const curry = (fn, arr = []) =>
(...args) =>
(arg => (arg.length === fn.length) ? fn(arg)):
curry(fn, arg)))([...arr, ...args]);
let curryTest = curry((a, b, c, d) => a + b + c + d);
curryTest(1, 2, 3)(4); // => 10
curryTest(1, 2)(4)(3); // => 10
curryTest(1, 2)(3, 4); // => 10
优点
柯里化是一种函数预加载的办法,通过传递觉少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的缓存。是一中非常搞笑的编写函数的方法。
柯里化和偏应用的区别
柯里化的参数是从左向右的,如果使用 setTimeout 这种就得额外的封装。偏应用函数是从右向左的。
const setTimeoutWraper = (timeout, fn) => {
setTimeout(fn, timeout);
}
const delaytenMS = curry(setTimeoutWraper)(10);
delayTenMS(() => console.log("do x Task"));
delayTenMS(() => console.log("do y Task"));
函数组合
纯函数以及柯里化写着写着就容易出现嵌套特别深的代码: h( g( f(x) ) ),为了解决这种嵌套,就需要用到函数组合。
通过代码来看:
const compose = (f, g) => (x => f(g(x)));
const first = arr => arr[0];
const reverse = arr => arr.reverse();
const last= compose(first, reverse);
last([1, 2, 3, 4, 5]);
其实就是把函数在内部执行一下。
compose(f, compose(g, h));
compose(compose(f,g), h);
compose(f, g, h)
函数组合会让内部的调用更加灵活,f 可以和 g 组合,f 也何以和 h 组合, h 和 g 也能够组合。
函数组合子
组合子(全称:组合子函数)又称之为装饰器函数,用于转换函数或数据,增强函数或数据行为的高阶函数。
函数组合数据流是从右至左的,因为最右边的函数首先执行,讲数据传递给下一个函数以此类推。可以用另一种方式左边的先执行。可以实现pipe来实现。它和compose所做的一样,只不过交换了数据方向。
因此我们需要组合子管理程序的控制流。
命令式的代码能够使用 if..else 和 for 这样的过程控制,函数式则不能。所以需要函数组合子。组合子可以组合其他函数或者其他组合子,并作为控制逻辑单元的高阶函数,组合子通常不声明任何变量,也不包含任务业务逻辑,他们目的是在管理函数程序执行流程。并在链式调用中对中间结果进行操作。
常见组合子如下
辅助组合子
无为(nothing)、照旧(identity)、默许(defaultTo)、恒定(always)
函数组合子
收缩(gather)、展开(spread)、颠倒(reverse)、左偏(partial)、右偏(partialRight)、柯里化(curry)、弃离(tap)、交替(alt)、补救(tryCatch)、同时(seq)、聚集(converge)、映射(map)、分捡(useWith)、规约(reduce)、组合(compose)
谓语组合子
其他
组合子变换juxt
分属于SKI组合子。(通过运算达到指定的目的)
Point Free
在实现函数式编程的时候不要有过多的中间变量。
通过代码来看:
const f = str => str.toUpperCase().split('');
// 正确书写格式
const toUpperCase = word => word.toUpperCase();
const split = x => (str => str.split(x))
const f = (split(''), toYpperCase);
f('abcd efgh');
声明式与命令式
命令式就是通过一条一条指令去执行一些动作。生命是就是通过表达式来声明干什么。
// 命令式
let ceo = [];
for(let i; i< companies.length;i++){
ceo.push(companies[i].value);
}
//声明式
let ceo = companies.map(item => item.value);
优点
声明式的代码对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务。优化代码时目光只需要集中在稳定兼顾的函数内部即可。
惰性链、惰性求值、惰性函数
_chain(data).map().reverse().value() 惰性链可以添加一个输入对象的状态,从而能够将这些输入转换为所需的输出操作连接在一起。在最后调用 value 之前并不会真的执行任何操作。这就是惰性链。
当输入很大但只有一个子集有效时,避免不必要的函数调用就是惰性求值。尽可能的推迟求值,直到以来的表达式被调用。
加入同一个函数被大量范围,并且这个函数内部又有许多判断来检测函数。这昂对于一个调用会浪费时间和浏览器资源,所以当第一次判断完成之后,直接把这个函数改写,不需要判断。
//惰性求值
const alt = _.curry((fn1, fn2, val) => fn1(val) || fn2(val));
const showStudent = _.compose(函数体1, alt(xx1, xx2));
showStudent({});
let object = {a: 'xx', b: 2};
let values= _.memoize(_.values);
values(object);
object.a = '京程一灯';
console.log(values.cache.get(object));
// 惰性连
//_.chain可以推断可优化点,如合并执行后存储优化
//合并函数执行 并压缩计算过程中的临时数据结构降低内存占用
const trace = msg => console.log(msg);
let square = x => Math.pow(x, 2);
let isEven = x => x % 2 === 0;
// 使用组合子跟踪
square = R.compose(R.tap(()=>trace("map数组")), square);
isEven = R.compose(R.tap(()=>trace("filter数组")), isEven);
const numbers = _.range(200);
const result = _.chain(numbers)
.map(square)
.filter(isEven)
.take(3)
.value();
console.log(result);
/*
* 首先take(3)只担心前三个通过的map和filter的值
* 其实shortcut fusion技术把map和filter融合
* _.compose(filter(isEven, map(square));
* compose(filter(p1), filter(p2) => filter(x) => p1(x) && p2(x))
* Lodash中其他带由shortcut fusion优化的函数 _.frop...
* _.drop、_.dropRight、_.dropRightWhile、_.dropWhile...
*/
//惰性函数
function crreateXHR() {
var xhr = null;
if (typeof XMLHttpRequest != 'undefined') {
xhr = new XMLHttpRequest();
createXHR = function () {
return XMLHttpRequest():
}
} else {
try {
xhr = new ActiveXObject("Msxml2.HMLHTTP");
createXHR = function () {
return new ActiveXObject("Msxml2.XMLHTTP");
}
} catch (e) {
try {
xhr = new ActiveXObject("Microsoft.HMLHTTP");
createXHR = function () {
return new ActiveXObject("Microsoft.XMLHTTP");
}
} catch (e) {
createXHR = function () {
return null;
}
}
}
}
}
高阶函数
函数当参数把传入的函数做一个封装,然后返回这个封装函数,达到更高程度的抽象。
通过代码来看:
let add = function (a, b) {
return a + b;
}
function math(func, array) {
return func(array[0], array[1]);
}
math(add, [1,2]); // => 3
- 它是一等公民
- 一个函数作为参数
- 返回一个函数作为结果
尾调用优化PTC
指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数,函数调用自身,成为递归。如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出,如果使用尾递归优化,将递归变为循环,那么值需要保存一个调用记录,就不会发生栈溢出。
尾递归的判断标准是最后一步调用自身,而不是最后一行调用自身,最后一行调用其他函数并返回叫尾调用。// 递归 无优化
function factorial(n) {
if(n === 1) return 1;
return n* factorial(n - 1);
}
// 尾递归 有优化
function factorial(n ,total) {
if(n === 1) return total;
return factorial(n - 1, n * total);
}
闭包
闭包是函数式编程得重要核心,没有闭包就没有函数式编程。 ```javascript function makePowerFn(power) { function powerFn(base){ return Math.pow(base, power); } return powerFn; }
const square = makePowerFn(2); square(3); // => 9
<a name="z2pis"></a>
## 容器和函子 (Functor)
范畴其实就是一个容器,里面包含两样东西。值 和 成员变形关系,也就是函数。<br />范畴论使用函数,表达范畴之间得关系。
函数不仅可以可以用于同一个范畴之中值得转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。<br />函子是函数式编程里面最终要得数据类型,也是基本得运算单位和功能单位。它首先是一种范畴,也就是说是一个容器,包含了值和变形关系。比较特殊的是它的变形关系可以一次作用于每一个值,将当前容器变形成另一个容器。<br />函子是遵守特定规则的容器。
通过代码来看:
```javascript
const Container = function(x) {
this.value = x;
}
Container 就是一个容器。
把Container变成一个函子:
const Container = function(x) {
this.value = x;
}
Container.of = x => new Container(x);
Container.prototype.map = function(f) {
return Container.of(f(this.value))
}
Container.of(3).map(x => x + 1).map(x => 'Result is' + x);
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值映射到另一个容器。这里 f 就是变形关系,作用于自己的 value,让后又创建一个新的容器。map每次返回一个 Container ,它把x变成了 Container(4) 和 Container(‘Result is 4’)
ES6这样写:
class Functor{
constroctor(val){
this.val = val;
}
map(f) {
return new Functor(f(this.val));
}
}
(new Functor(2)).map(x => x + 2);
它会返回一个 Functor(4) 。
pointed函子
函子只是实现了map契约的接口。Pointed 函子是一个函子的子集。
生成新的函子的时候,用了 new 命令。这实在不太像函数式编程,因为 new 命令是面向对象编程的标志。函数式编程一般约定,函子又一个 of 方法,用来生成新的容器。
Functor.of = function (val){
return new Functor(val);
}
Maybe函子
Maybe 用于处理错误和异常。函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值,而外部的函数未必有处理空值的机制,如果传入空值,很可能会出错。
Functor.of(null).map(s => s.toUpperCase());
class Maybe extends Funtor {
map(f) {
return this.val? Maybe.of(f(this.val)):Maybe.of(null);
}
}
Maybe.of(null).mao(s => s.toUpperCase());
Either函子
Either 函子跟 Maybe 比较类似,主要用来 try / catch / throw。
Promise 是可以调用 catch 来集中处理错误的。
事实上 Either 并不是只用来做错误处理的,它表示了逻辑或范畴学里的 coproduc。
class Either extends Functor {
constructor(left, right){
this.left = left;
this.right = right
}
map(f){
return this.right?
Either.of(this.left, f(this.right)):
Either.of(f(this.left), this.right);
}
}
Either.of = function(left, right) {
return new Either(left, right);
}
const addOne = x => x + 1;
Either.of(5, 6).map(addOne); // => Either(6, 7)
Either.of(1, null).map(addOne); // => Either(2, null)
Either.of({address: 'xxx'}, currentUser.address).map(updateFiled);
AP函子
函子里面包含的值,完全可能是函数。我们可以想象一下,一个函子的值是数字,另一个函子的值是函数。AP 函子主要用来解决 value 是函数怎么办。
class Ap extends Functor {
ap(f) {
return Ap.of(this.val(f.val))
}
}
IO函子
真正的程序总是不可能完全纯的。
function readLoacalStorage(){
return window.localStorage;
}
IO跟前面那几个 函子 不同的地方在于,它的 value 是一个函数。 它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作执行。所以 IO 包含的是被包裹的操作的返回值。
IO也算是惰性求值。
IO负责了调用链积累了很多不纯的操作,带来的复杂性和不可维护性。
import _ from 'lodash';
const compose = _.flowRight;
const IO = function(f) {
this.value = f;
}
IO.of = x => new IO(_ => x);
IO.prototype.map = function(f) {
return new IO(compose(f, this.value));
}
Monad函子
函子是一个容器,可以包含任何值。函子之中再包含一个函子,也是完全合法的。但是,这样就会出现多层嵌套的函子。
Monad 函子的作用是,总是返回一个单层的函子。它就解决了函子嵌套的问题。Pomise就是一种Monad。可以一路 then 下去。
var fs = require('fs');
var _ = require('lodash');
class Functor{
constructor(val) {
this.val = val;
}
static of(val){
return new Functor(val);
}
map(f) {
return new Functor(f(this.val));
}
}
class Monad extends Functor {
join() {
return this.val;
}
flatMap(f) {
/*
* f == 接受一个函数返回的是 IO 函子
* this.val 等于上一步的脏操作
* this.map(f) compose(F, this.val) 函数组合 需要手动执行
* 返回这个函数组合并执行 注意先后的顺序
*/
return this.map(f).join();
}
}
var compose = _.flowRight;
class IO extends Monad {
map(f) {
return IO.of(compose(f, this.val));
}
}
var readFile = function (filename) {
return IO.of(function (){
return fs.readFileSync(filename, 'utf-8');
})
}
var print = function (x){
cosnole.log(111);
return IO.of(function (){
console.log(222);
return x + '函数式';
})
}
var tail = function (x){
console.log('tail');
return IO.of(function(){
retrun x + 'tail';
})
}
const result = readFile('./user.txt')
.flatMap(print)()
.flatMap(tail)();
console.log(result().val());