范畴论
所有的概念体系都可以抽象出一个个范畴;
彼此之间存在某种关系,感念,事务,对象等都构成范畴,任何事物只要找出他们之间的关系就能定义;
- 范畴成员之间的关系叫做态射,范畴论任务同一个范畴的所有成员,就是不同状态的变形,通过态射一个成员可以变形成另一个成员。
- 一个成员通过变形成为另一个成员这个过程叫做函数,编程另一个成员之后原本的成员就不存在了;
函数式编程
- 来源于lambda演算,是一套用与研究函数定义,函数应用和递归的形成
函数式编程的目的
在于将复杂的函数复合成简单的函数(运算过程尽量写成一些列嵌套的函数调用)
函数是一等公民
一等公民的是以是指函数和其他数据类型一样处于平等地位,可以赋值给其他变量,也可以作为参数传入另一个函数,或者作为其他函数的返回值
不可变量
在函数式编程中,我们通常理解的变量在函数式编程中也被函数代替了,在函数式编程中变量仅仅代表某个表达式,这里所说的‘变量’是不能被修改。所有的变量只能被复制一次初值;
常用的函数式编程的方法
- map
var arr = [2,4,6] ;
var ar = arr.map(function(item){
return item*item ;
}) ;
console.log( '原数组' , arr ) ; //原数组 [2, 4, 6]
console.log( '新数组' , ar ) ; //新数组 [4, 16, 36]
- reduce
var arr = [1,2,3,4,5] ;
var ar = arr.reduce( function( index , item ){
console.log( 'a' , index , item )
return index + item ;
}) ;
console.log( ar ) ; //15
纯函数
- 对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态
//相同的输入有相同的输出,
var arr = [1,2,3,4,5] ;
var ar = arr.slice(0,3); //123, 相同的输入结果永远一样
console.log( arr ) // [1,2,3,4,5] //原数组没有被改变,叫做没有副作用
函数组合
达到某个目的需要两个过程A和B;
如:计算一个学生的平均分
function sum( arr ){
var sum = 0 ;
for( var i=0 ; i<arr.length ; i++ ){
sum += arr[i] ;
} ;
return { length :arr.length , sum:sum } ;
} ;
function avg( obj ){
return obj.sum/obj.length ;
} ;
function getAvg( avg , sum ){
return function( x ){
return avg( sum(x) );
} ;
} ;
var std = [ 80 , 90 , 100 ] ;
var stdAvg = getAvg( avg , sum );
console.log( stdAvg(std) ); //90
函数柯里化
柯里化是一种‘预加载’函数的方法,通过传递较少的参数得到一个已经记住这些参数的新函数,某种意义上,这是一种对参数的缓存,是一种非常高效的编写函数的方法
例:
function sum( x , y ){
return x + y ;
} ;
sum( 1 , 2 ) ; //3
对函数进行柯里化
functiom sum(x){
return function(y){
return x + y ;
}
}
//延迟执行需要执行的业务逻辑;
var s = sum(1); //先将1缓存起来,在需要的时候再调用s
s(2); //3
声明式与命令式代码
- 命令式代码的意思是通过编写一条又一条指令让计算机执行一些动作,这其中一般都会涉及到很多繁杂的细节
let students = [] ;
let schools = [
qinghua:{ students:100 , teacher:20 } ,
beida:{ students:200 , teacher:50 }
] ;
for( let i=0 ; i<schools.length ; i++ ){
students.push( schools.students );
} ;
- 声明式:通过写表达式的方式来声明我们想干什么而不是通过一步一步的指示
let students = schools.map(function(item){
return item.students ;
});
函数式编程的优点
- 声明式的代码对于无副作用的纯函数,完全可以不考虑函数内部如何实现。优化代码时只需集中在这些稳定坚固的函数内部即可。
- 不纯的函数式的代码会产生副作用或依赖外部系统环境,使用的时候总是需要考虑这些不干净的副作用,在复杂的系统中会拖累思维。
惰性函数,惰性求值
var eventListener = (function(){
if( window.addEventListener ){
return function( ele , type , fn ){
ele.addEventListener( type , fn , false );
}
}else{
return function( ele , type , fn ){
ele.attachEvent('on'+type , fn.bind(ele,window.event) );
}
}
})()
高阶函数
- 函数当参数,吧传入的函数做一个封装,然后返回这个封装函数达到更高程度的抽象
var add = function( a , b ){
return a + b ;
} ;
function sum( fn , arr ){
return fn( arr[0] , arr[1] );
} ;
sum( add , [1,2] ); //3
尾递归
尾递归是指函数内部的最后一个动作是函数调用,该调用的返回值直接返回给函数。函数调用自身称为递归,如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出,如果使用尾递归优化,将递归变为循环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误
不是尾递归
//最后一个动作不是调用函数,而是 n + fn() , 所以不是尾递归
function fn(n){
if( n == 1 ) return 1 ;
return n * fn(n);
}
- 尾递归
//这是尾递归
function fn( n , total ){
if( n == 1 ) return total ;
return fn( n-1 , n*total );
} ;
闭包
function fn(n){
return function(m){
return n.Math.pow(m);
}
}
//n不会被释放掉
范畴与容器
- 可以把范畴想象成一个容器,里面包含两样东西,值,值的变形关系(函数) ;
- 范畴论使用函数表达范畴之间的关系;
- 伴随范畴论就发展出一套函数的运算方法,用计算机表达出来就是函数式编程;
- 本质上,函数式编程知识范畴论的运算方法,跟数理逻辑,微积分,行列式是同一类都是数学方法,只是碰巧可以用程序来表达。为什么函数式编程要求函数必须是纯的,不能有副作用;因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则。
函子
- Funcftor 函子,遵守一些特定规则的容器类型;
- 函子( Functor ) 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力,把东西装进一个容器,只留一个借口map给容器外的函数,这样容器就可以自由地选择何时何地如何操作这个函数,以至于拥有惰性求值,错误处理,异步调用等很好的特性;
class Functor {
constructor(val){
this.val = val ;
}
map(f){
return new Functor( f(this.val) );
}
}
上面的代码中,Functor是一个函子,它的map方法接收函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的
一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值映射到另一个容器;
上面的例子说明,函数式编程里的运算都是通过函子完成,即运算不直接针对值,而是针对这个值的容器–函子。函子本身具有对外接口(map方法),各种函数就是运算符,通过接口接入容器,引发容器里面的值的变形。
因此学习函数式编程实际上就是学习函子的各种运算,由于可以把运算方法封装在函子里面,所以又衍生出各种不同类型的函子,有多少种运算,就有多少种函子。函数式编程就变成了运用不同的函子,解决实际问题。