函数式编程基础理论
1.数学中的函数书写如下形式 f(x) = y。 一个函数F,已x作为参数,并返回输出y。这很简单,但是包含几个关键点
函数必须总是接受一个参数 函数必须返回一个值 函数应该依据接收到的参数 而不是外部环境运行,对于给定的X 只会输出唯一的y
2.函数式编程不是用函数来编程 也不是传统的面向过程编程。主旨在于将复杂的函数合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。
运算过程尽量写成一系列嵌套的函数调用。
3.通俗写法 function xx(){} 区别开函数和方法。方法要与指定的对象绑定、函数可以直接调用
1 函数式一等公民 所谓第一等公民 指的是函数和其他数据类型一样 ,处于平等地位 可以赋值给其他变量也可以作为参数,传入另一个参数,或者作为别的函数的返回值
2 不可改变量。在函数编程中我们通常理解的变量在函数式编程中也被函数代替了,在函数式编程中变量仅仅代表某个表达式。这里所说的变量是不能被修改的。所有的变量只能被赋值一次初值
3. map & reduce他们是最常用的函数式编程方法。
1.函数是第一等公民
2.只用表达式,不用语句
3.没有副作用
4.不修改状态
5.引用透明(函数运行只靠参数,且相同的输入总是获得相同的输出)identity = (i)=>{return i} 调用identity(7)可以直接替换7,该过程被称为替换模式
范畴论
1 函数式编程是范畴论的数学分支 是一门很复杂的数学,
认为世界上所有概念体系都可以抽象出一个个范畴
2 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
3 箭头表示范畴成员之间的关系。正式的名称叫做态射。
范畴轮认为,同一个范畴的所有成员,就是不同状态的变形。通过态射,一个成员可以变形成另一个成员
纯函数
// 输入仅由输入决定,且不产生副作用
const greet = (name)=>`hello,${name}`
// 以下代码不是纯函数
window.name = 'Brinse'
const greet = ()=>`hello,${window.name}`
greet() //hi, Brinse
// 以上示例中,函数依赖外部状态
let greeting
const greeting = (name)=>{
greeting = `Hi,${name}`
}
greet('Brinse')
greeting//hi,Brinse
以上实例中,函数修改了外部状态。
如果函数与外部可变状态进行交互,则它是有副作用的
对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态
var xs = [1,2,3,4]
//Array slice是纯函数 因为它没有副作用,对于固定的输入 输出总是固定的
// splice则不是,它会改变原数组
xs.slice(0,3)
xs.slice(0,3)
xs.splice(0,3)
xs.splice(0,3)
优缺点
纯函数不仅可以有效降低系统的复杂性,还有很多很棒的特性,比如可缓存性
Math.sin 用于计算正弦 指的是“弧度”
import _ fromm 'lodash'
var sin = _.memorize(x=>
Math.sin(x))
// 第一次计算的时候会稍慢一点
var a = sin(7)
// 第二次有了缓存,速度极快
var b = sin(7)
不纯的
var min = 18
var checkAge = age =>age>min
// 纯的、
var checkAge = age=>age>18
在不纯的版本中 checkAge不仅取决于age还有外部依赖的变量min
纯的checkage把关键字18硬编码在函数内部,扩展性比较差 柯里化优雅的函数式解决
纯度和幂等性
幂等性是指执行无数次后还具有相同的效果 同一的参数运行一次函数与连续两次结果一致
幂等性在函数式编程中与纯度相关 但有不一致
Math.abs(Math.abs(-47))
幂等性
如果一个函数执行多次皆返回相同的结果 则它是幂等性的
f(f(x)) = f(x)
Math.abs(Math.abs(10))
sort(sort(sort([1,2,0])))
偏应用函数
传递给函数一部分参数调用它,让它返回一个函数去处理剩下的参数
偏函数之所以偏 就在于其只能处理那些与至少一个case语句匹配的输入 而不能处理所有的输入
偏应用函数(partial application)
带一个函数参数 和该函数的部分参数
const partial = (f,...args)=>(...moreArgs)=>f(...args,...moreArgs)
const add3 = (a,b,c)=>a+b+c
//偏应用函数2和3 到add 3给你一个单参数的函数
const fivePlus = partial(add3,2,3)
fivePlus(4)
//bind实现
const add1More = add3.bind(null,2,3)
偏函数应用通过对复杂的函数填充一部分数据来构成一个简单的函数。柯里化通过偏函数实现
函数的柯里化
柯里化(curried) 通过偏应用函数实现,它是把一个多参数函数转换为嵌套一元函数的过程
传递给函数一部分参数来调用它 让它返回一个函数去处理剩下的参数。
我们一起采用柯里化来改它
var checkAge = min => age=>age>min
var checkAge18 = checkAge(18)
checkAge18(20)
函数的反柯里化
函数柯里化,是固定部分参数,返回一个接受剩余参数的函数 也称为部分计算函数,目的是为了缩小适用范围,创建
一个针对性更强的函数
那么反柯里化函数 从字面讲 意义和用法根函数柯里化相比正好相反 扩大适用范围,创建一个应用范围更广的函数。
使本来只有特定对象才适用的方法 扩展到更多的对象
函数的反柯里化Code
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) //{0: 'first', 1: 'two', length: 2}
函数的柯里化Code
接受部分参数 然后返回一个新函数等待接受剩余参数,递归直到接收到全部所需参数,最后执行
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
lodash中的柯里化
import (curry) from "lodash"
var match = curry((reg,str)=>str.match(reg))
var filter = curry((f,arr)=>arr.filter(f))
var haveSpace = match(/\s+/g)
// havaSpace('ffff')
// havaSpace('a b')
// filter(haveSpace,['abcs','HEllo World'])
filter(haveSpace)(['abcdse','Hello World'])
事实上柯里化是一种预加载函数的方式 通过传递较少的参数
得到一个已经记住了这些参数的新函数,某种意义上讲这是,一种对参数的缓存,是一种非常高效的编写函数的方法
函数的柯里化Code
function foo(p1,p2){
this.val = p1 + p2
}
// bind 返回一个函数
var bar = foo.bind(null,'p1')
var baz= new bar('p2')
console.log(baz.val);
柯里化和偏应用的区别
柯里化的参数列表是从左往右的,如果使用setTimeout这种就得额外的封装。
const setTimeoutWraper=(timeout,fn)=>{
setTimeout(fn,timeout);
}
const delayTenMs = curry(setTimeoutWraper)(10)
delayTenMs(()=>console.log('do Xtask'))
delayTenMs(()=>console.log('do Ytask'))
setTimeoutWraper显得多余,这时候我们就可以使用偏函数,使用curry和partial是为了让函数参数或函数设置变得更加简单和强大 curry、和partial实现也可以参考lodash
函数的柯里化Code
//柯里化之前
function add(x,y){
return x+ y
}
add (1,2)//3
// 柯里化之后
function addX(y){
return function(x){
return x + y
}
}
addX(1)(2) //3
函数组合
纯函数以及如何把它柯里化写出洋葱代码(h(g(f(x)))),
为了解决函数嵌套的问题,我们需要用到函数组合:
我们一起来用柯里化来改动他,让多个函数像拼积木一样
const compose = (f,g)=>(x=>f(g(x)))
var first = arr =>arr[0]
var reverse = arr =>arr.reverse()
var last = compose(first,reverse)
last([1,2,3,4,5])
函数组合
compose(f,compose(g,h))
compose(compose(f,g),h)
compose(f,g,h)
函数组合子
compose函数只能组合接受一个参数的函数,类似于filter,map
接受两个参数(投影函数:总是在应用转换操作,通过传入高阶参数后返回数组),
不能被直接组合可以借助偏函数包裹后继续组合
函数组合的数据流是从右到左,因为最右边的函数首先执行,
将数据传递给下一个函数以此类推,有人喜欢另一种方式最左侧的先执行,我们可以实现pipe(可称为管道,序列)来实现。
它和compose所做的事情一样 只不过交换了数据方向,
因此我们需要组合子管理程序的控制流
函数组合子
命令式代码能够使用if-else和for这样的过程控制
函数式则不能。所以我们需要函数组合子,函数组合子可以组合其他函数(或其他组合子)
并作为控制逻辑单元的高阶函数,组合子通常不声明任何变量,也不包含任何业务逻辑 他们旨在管理函数程序执行流程
并在链式调用中对中间结果进行操作
常见的组合子如下:
– 辅助组合子
无为(nothing)、照旧(identity)、默许(default)、恒定(always)
函数组合子
收缩(gather) 展开(spread)、 颠倒(reverse)、左偏(partial)、右偏(partialRight)、
柯里化(curry)、弃离(tap),交替(alt)、补救(tryCatch)、同时(req)、聚集(converse)
映射(map)、分拣(useWith)、规约(reduce)、组合(compose)
谓语组合子
过滤(filter) 、分组(group)、排序(sort)
其他
组合子变换(juxt)
分属于变换juxt
分属于SKI组合子
Point free
把一些对象自带的方法转换成纯函数,不要命名转瞬即逝的中间变量
这个函数中 我们使用了str作为我们的中间变量,但这个中间变量除了让代码更长一点
之外毫无一点用处
const f = str =>str.toUpperCase().split('')
这种风格能够帮助我们减少不必要的命名 让代码保持简洁和通用
var toUpperCase = word=>word.toUpperCase()
var split = x =>(str=>str.split(x))
var f = compose(split(' '),toUpperCase)
f('abcd efgh')
Point-Free
定义函数时,不显式地指出函数所带参数。这种风格通常需要柯里化或者高阶函数
const map = (fn)=>(list)=>list.map(fn)
const add = (a)=>(b)=>a+b
// Point-Free list是显式参数
const incrementAll = (numbers)=>map(add(1))(numbers)
// Point-Free list是隐式参数
const incrementAll2 = map(add(1))
increment 识别并且使用了numbers参数,因此它不是Point-Free风格
incrementAll2连接函数与值 并不提及它使用的参数 因为它是Point-Free风格的
Point-Free风格的函数就像平常的赋值,不使用function或者 =>