Klaus-sun JavaScript 函数式编程 笔记
函数式编程思维 :主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用.
函数式编程基础理论
1 、纯函数
什么是纯函数?
对于相同的输入,永远的到相同的输出,而且没有任何可观察副作用, 也不依赖外部环境的状态的函数,叫做纯函数。
View2、函数柯里化
传递给函数的一部分参数来调用它,让它返回一个函数处理剩下的参数。
我们创建一个这样的函数 checkage;
1 const min = 18 ; // 常量min 赋值18 2 var checkage = age => age >min; 3 //这个函数并不纯, checkage 不仅取决于age 还有外部依赖的变量min。 4 // 纯的 checkage 把关键数字 18 硬编码在函数内部,扩展性比较差,柯里化优雅的函数式解决。 5 var checkage = min => (age => age > min); 6 var checkage18 = checkage(18); // 先将18作为参数,去调用此函数,返回一个函数age => age > 18; 7 checkage18(20);// 第二步,上面返回的函数去处理剩下的参数,即 20 => 20 > 18; return true;
我们举个例子
//柯里化前
function add(x,y){
return x+y;
}
add(1,2); //3
function addA(x){
return function(y){
return x+y;
}
}
addA(2)(3); // 5 addA(2)返回一个函数作为参数(y)执行运算结果
事实上柯里化是一种 “预加载” 函数的方法,通过传递较少的 参数得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法。
3、组合函数
为了解决函数嵌套过深,洋葱代码:h(g(f(X))),我们需要用到“函数组合”, 我们一起来用柯里化来改他,让多个函数像拼积木一样。
1 const compose = (f,g) => (x=>f(g(x))); 2 3 var first = arr => arr[0]; // 数组第一下 4 5 var reverse = arr =>arr.reverse(); //颠倒元素顺序 6 7 var last = compose(first,reverse); //接受一个数组 8 9 last ([1,2,3,4,5,6]) ; // 6 返回数组最后一项
函数组合交换律,类似于乘法交换律:
4、Point Free
把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。
toUpperCase方法将字符串小写字符转换为大写。split ' '区分 ,这个函数中,我们使用了 str 作为我们的中间变量,但这个中间变量除了让代码变得长了一点以外是毫无意义的。
1 var f = str => str.toUpperCase().split(' ');
下面我们用组合函数改造一下 const compose = (f,g) => (x=>f(g(x)));
1 var toUpperCase = word => word.toUpperCase();
2
3 var split = x => (str => str.split(x));
4
5 var f = compose(split(' '),toUpperCase);
6
7 f("abcd efgh"); //以空格区分 转化大小数组 ["ABCD", "EFGH"]
8 // 把一些对象自带的方法转化成纯函数,然后通过函数组合去调用,这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。是不是很方便!
5、 声明式与命令式代码
* 在我们日常业务开发中,写的代码绝大多数都未命令式代码;
* 我们通过编写一条又一条指令让计算机执行一些动作,这其中都会涉及到很多繁杂的细节。
* 而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,
* 而不是通过一步一步的指示。
1 //命令式
2 let CEOs = [];
3 var companies = [{CEO:"2222"},{CEO:"3333"}];
4 for (var i = 0; i < companies.length; i++) {
5 CEOs.push(companies[i].CEO)
6 }
7 //声明式
8 let CEOs = companies.map(c => c.CEO);
函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,
目光只需要集中在这些稳定坚固的函数内部即可。相反,不纯的函数式的代码会产生副作用或者依赖外部系统环境,使用它们的时候总是要考虑这些不干净的副作用。在复杂的系统中,这对于我们的心智来说是极大的负担。
6、核心概念
* 高阶函数
* 高阶函数,就是把函数当参数,把传入的函数做一个封装,
* 然后返回这个封装函数,达到更高程度的抽象。
1 //命令式
2
3 var add = function(a,b){
4 return a + b;
5 }
6 function math(func,array){
7 return func(array[0],array[1]);
8 }
9
10 math(add, [1, 2]); // 3
递归与尾递归
* 指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。
* 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,
* 很容易发生栈溢出错误,如果使用尾递归优化,姜帝圭变为循环,那么只需要保存一个调用记录,
* 这样就不会发生栈溢出错误了 。通俗点说,尾递归最后一步需要调用自身,并不能有其他额外操作。
1 //不是尾递归无法优化
2 function factorial(n){
3 if(n===1) return 1;
4 return n * factorial(n-1);
5 }
6
7 function factorial(n,total){
8 if(n===1){ return 1}
9 return factorial(n-1,n*total);
10 } // es6强制使用尾递归
11
12 function sum(n) {
13 if (n === 1) return 1;
14 return n + sum(n - 1);
15 }
16
17 sum(6)
输出
(6 + sum(5))
(6 + (5 + sum(4)))
(6 + (5 + (4 + sum(3))))
(6 + (5 + (4 + (3 + sum(2)))))
(6 + (5 + (4 + (3+ 2 sum(1)))))
(6 + (5 + (4 + (3 + 2 +1 ))))
(6 + 15)
21
1 //尾递归
2 function sum(x,total){
3 if(x===1){
4 return x + total;
5 }
6 return sum(x - 1, x + total);
7
8 }
整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层sum(5,0)。这能有效的防止堆栈溢出。
在ECMAScript 6,我们将迎来尾递归优化,通过尾递归优化,javascript代码在解释成机器码的时候,将会向while看起,也就是说,同时拥有数学表达能力和while的效能
(5+4+3+2+1) //15
范畴与容器
1.函数不仅可以用于同一个范畴之中的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了函子(Functor)。
2.函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,
它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。容器与函子(Functor)$(...) 返回的对象并不是一个原生的 DOM 对象,
而是对于原生对象的一种封装,这在某种意义上就是一个“容器”(但它并不函数式)。Functor(函子)遵守一些特定规则的容器类型。
任何具有map方法的数据结构,都可以当作函子的实现。
functor是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。
* 把东西装进一个容器,只留出一个接口 map 给容器外的函数,map 一个函数时,我们让容器自己来运行这个函数,这样容器就可以自由地选择何时何地如何操作这个函数,
* 以致于拥有惰性求值、错误处理、异步调用等等非常牛掰的特性。
1 Container.prototype.map = function (f) {
2 return Container.of(f(this.__value))
3 }
4 Container.of(3).map(x => x + 1) //=> Container(4)
5 .map(x => 'Result is ' + x); //=> Container('Result is 4')
6
7 class Functor {
8 constructor(val) {
9 this.val = val;
10 }
11 map(f) {
12 return new Functor(f(this.val));
13 }
14 }
15 (new Functor(2)).map(function (two) {
16 return two + 2;
17 });
上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的(f(this.val))。
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器。上面的例子说明,函数式编程里面的运算,都是通过函子完成,即运算不直接针对值,而是针对这个值的容器----函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此,学习函数式编程,实际上就是学习函子的各种运算。由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。
你可能注意到了,上面生成新的函子的时候,用了new命令。这实在太不像函数式编程了,因为new命令是面向对象编程的标志。函数式编程一般约定,函子有一个of方法,用来生成新的容器。
Maybe 函子
函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错。
1 var Maybe = function (x) {
2 this.__value = x;
3 }
4 Maybe.of = function (x) {
5 return new Maybe(x);
6 }
7 Maybe.prototype.map = function (f) {
8 return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
9 }
10 Maybe.prototype.isNothing = function () {
11 return (this.__value === null || this.__value === undefined);
12 }
13 //新的容器我们称之为 Maybe(原型来自于Haskell,Haskell是通用函数式编程语言)
1 Functor.of(null).map(function (s) {
2 return s.toUpperCase();
3 });
4 // TypeError
5
6 Maybe.of(null).map(function (s) {
7 return s.toUpperCase();
8 });
9 // Maybe(null)
错误处理、Either函子
我们的容器能做的事情太少了,try/catch/throw 并不是“纯”的,因为它从外部接管了我们的函数,并且在这个函数出错时抛弃了它的返回值。Promise 是可以调用 catch 来集中处理错误的。事实上 Either 并不只是用来做错误处理的,它表示了逻辑或。
条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值。
1 class Either extends Functor {
2 constructor(left, right) {
3 this.left = left;
4 this.right = right;
5 }
6 map(f) {
7 return this.right ?
8 Either.of(this.left, f(this.right)) :
9 Either.of(f(this.left), this.right);
10 }
11 }
12 Either.of = function (left, right) {
13 return new Either(left, right);
14 };
使用Either函子:
1 var addOne = function (x) {
2 return x + 1;
3 };
4 Either.of(5, 6).map(addOne);
5 // Either(5, 7);
6 Either.of(1, null).map(addOne);
7 // Either(2, null);
8 Either
9 .of({
10 address: 'xxx'
11 }, currentUser.address)
12 .map(updateField);
AP函子
函子里面包含的值,完全可能是函数。我们可以想象这样一种情况,一个函子的值是数值,另一个函子的值是函数。
1 class Ap extends Functor {
2 ap(F) {
3 return Ap.of(this.val(F.val));
4 }
5 }
6
7 function addOne(x) {
8 return x + 1;
9 }A
IO函子
真正的程序总要去接触肮脏的世界。
function readLocalStorage(){
return window.localStorage;
}
IO 跟前面那几个 Functor 不同的地方在于,它的 __value 是一个函数。它把不纯的操作(比如 IO、网络请求、DOM)包裹到一个函数内,从而延迟这个操作的执行。所以我们认为,IO 包含的是被包裹的操作的返回值。
IO其实也算是惰性求值。
IO负责了调用链积累了很多很多不纯的操作,带来的复杂性和不可维护性
1 class IO extends Monad {
2 map(f) {
3 return IO.of(compose(f, this.__value))
4 }
5 }
在这里本文记录函数编程的学习记录 希望大家留言一起学习进步。


浙公网安备 33010602011771号