纯函数
相同的输入永远会得到形同的输出。而且没有任何副作用,也不依赖外部环境的状态。
例如: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); // => 10curryTest(1, 2)(4)(3); // => 10curryTest(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 />函子是遵守特定规则的容器。通过代码来看:```javascriptconst 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());
