函数式编程上
基础理论
范畴论Category Theory
- 函数式编程是范畴论的数学分支是一门很复杂的数学,认为世界上所有概念体系都可以抽象出一个个范畴
- 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义
- 箭头表示范畴成员之间的关系,正式的名称叫做"态射"(morphism)。范畴论认为,同一个范畴的所有成员,就是不同状态的"变形"(transformation)。通过"态射",一个成员可以变形成另一个成员。
一句话总结: 成员都是一个集合,变形关系是一个函数
函数式编程基础理论
- 数学中的函数书写如下形式f(X)=Y。一个函数F,已X作为参数,并返回输出Y。这很简单,但是包含几个关键点函数必须总是接受一个参数、函数必须返回一个值、函数应该依据接收到的参数(如X)而不是外部环境运行、对于给定的X只会输出唯一的Y。
- 函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论,或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用
- 通俗写法 function xx(){}区别开函数和方法。方法要与指定的对象绑定、函数可以直接调用。
- 函数式编程(Functional Programming)其实相对于计算机的历史而言是一个非常古老的概念,甚至早于第一台计算机的诞生。函数式编程的基础模型来源于 λ (Lambda x=>x*2)演算,而 λ 演算并非设计于在计算机上执行,它是在 20 世纪三十年代引入的一套用于研究函数定义、函数应用和递归的形式系统。
- JavaScript 是披着 C 外衣的 Lisp。
- 真正的火热是随着React的高阶函数而逐步升温。
了解一下即可,只需要知道函数式编程由来以久。 在近几年随着
react崛起而被带动渐渐火起来, 后来越来越多的框架和打包工具开始推崇。
函数式编程的原则
1. 函数是”第一等公民”
2. 只用”表达式",不用"语句"
3. 没有”副作用"
4. 不修改状态
5. 引用透明(函数运行只靠参数且相同的输入总是获得相同的输出)identity=(i)=>{return i} 调用identity(1)可以直接替换为1 该过程被称为替换模型
前端所接触到函数式编程
- 纯函数
- 偏应用函数、函数的柯里化
- 惰性求值
- 函数组合
- Point Free
纯函数
相同的输出总是有相同的结果,无副作用,不依赖外部变量。- 举个例子
/* 纯函数 */
const add = (n, m) => n + m
/* 纯函数,每次返回相同的结果 */
Array.proptotype.slice
/* 不纯, 因为改变了原数组, 返回的结果不一样 */
Array.proptotype.splice
- 纯函数有什么好处?
- 打包优化,在webpack5引入的prepack理念
- 热代码优化, 在V8中一段持续执行的代码会被直接转为字节码常驻于内存中
- 缓存优化, 基于一个输入对应一个输出的基础上. 理论上每个输入都只需要执行一次
/*
lodash.memoize 方法就是将传入纯函数的执行结果进行缓存
*/
import { memoize } from 'lodash'
const sqrt = memoize(x => Math.sqrt(x))
// 第一次执行耗时
console.time('首次执行')
sqrt(9999)
console.timeEnd('首次执行')
// 第二次执行
console.time('第二次执行')
sqrt(9999)
console.time('第二次执行')
纯但一定幂等
幂等性是指执行无数次后还具有相同的效果,同一的参数运行一次函数应该与连续两次结果一致。幂等性在函数式编程中与纯度相关,但也有不一致的情况。- Math.abs 求绝对值
- 4%2 8%2 取模
- ...
偏应用函数(partialapplication)
传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。偏函数之所以“偏”,在就在于其只能处理那些能与至少一个case语句匹配的输入,而不能处理所有可能的输入。
- 示例
/* 接受一个name和msg参数,在控制台中打印 ${name}: ${msg} */
const say = (name, msg) => console.log(`${name}: ${msg}`)
/**
* 接受一个函数并处理部分参数
* partial就是一个偏应用函数, 把name偏应用到say函数
*/
const partial = (fn, ...args) => function (...newArgs) { return fn(...[...args, ...newArgs]) }
// 得到只接受单参数的偏函数
const XMSay = partial(say, '小明')
const XHSay = partial(say, '小红')
XMSay('hello') // 小明: hello
XHSay('hi') // 小红: hi
- 理论与实践不一定合法, 查看jQuery的偏函数使用.pdf
函数柯里化就是偏函数的实现
- 柯里化(Curried)通过偏应用函数实现。它是把一个多参数函数转换为一个嵌套一元函数的过程。
- 传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
现实一个柯里化
const min = 18
/* 不纯,依赖了外部变量 */
const checkAge = age => age < min
/* 变成纯函数 */
const checkAge2 = (min, age) => age < min
/* 柯里化 */
const checkAge3 = checkAge2.bind(null, 18)
lodash的实现
/* 抄录lodash的代码片段 */
var abc = function(a, b, c) {
return [a, b, c];
};
var curried = lodash.curry(abc);
curried(1)(2)(3);
// => [1, 2, 3]
curried(1, 2)(3);
// => [1, 2, 3]
curried(1, 2, 3);
// => [1, 2, 3]
核心源码实现
/* 函数柯里化 */
const curry = (fn, ...args) => function (...newArgs) {
/* 收集参数合并 */
const curArg = [...args, ...newArgs]
/* 形参个数与实参个数相等 */
if (curArg.length === fn.length) return fn(...curArg)
/* 尾递归继续收集 */
return curry(fn, ...curArg)
}
实际上柯里化是一种“预加载”函数的方法,通过传递较少的参数,得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种对参数的“缓存”,是一种非常高效的编写函数的方法
柯里化和偏函数的区别
柯里化和偏应用无非就是对参数的预处理的手段,目的就是使参数传递以及函数组合变得更灵活。柯里化是收集完所有的参数进行统一处理, 偏函数是边收集边处理并返回一个新的处理函数。二者本质上都可以说是惰性的
惰性求值
惰性链、惰性求值、惰性函数、
当输入很大但只有一个小的子集有效时,避免不必要的函数调用就是所谓的惰性求值。/* 伪代码 */
[1, 2, 3, 4, 5].map(处理函数).reverse(反转).slice(0, 3) // 只要取前3个
/* 使用惰性求值进行的优化 */
[1, 2, 3, 4, 5].map(处理函数).reverse(反转).slice(0, 3).value()
=>
/* 惰性求值会将一些操作合并减支,当调用 .values 操作才返回值 */
[1, 2, 3, 4, 5].slice(-3).map(处理函数)
lodash实战
import _ from 'lodash'
var users = [
{ 'user': 'barney', 'age': 36 },
{ 'user': 'fred', 'age': 40 },
{ 'user': 'pebbles', 'age': 1 }
];
/* _.chain创建了一个惰性链,会自动推断可以优化的点以及合并操作 */
var youngest = _.chain(users)
.sortBy('age')
.map(function(o) {
return o.user + ' is ' + o.age;
})
.head()
.value()
函数组合子
命令式与声明式代码
const arr = [1, 2, 3, 4, 5]
const con = new Array(arr.length)
/* 命令式代码 */
for (let i = 0, len = arr.length; i < len; i++) {
con[i] = arr[i]
}
/* 声明式 */
const con2 = arr.map(x => x)
函数式编程的一个明显的好处就是这种声明式的代码,对于无副作用的纯函数,我们完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
声明式代码方便我们随意组合
- 简单示例
/*
compose函数只能组合接受一个参数的函数,类似于filter、map 接受两个参数(投影函数:总是在应用转换操作,通过传入高
*/
const compose= (f,g) => ( x => f(g(x)));
/* 简单示例 */
const first = arr => arr[0];
const reverse = arr => arr.reverse();
const last = compose(first,reverse);
console.log(last([1,2,3,4,5])) // 5
- 简单进阶
const data = [
"life is a stream",
"on which we strew",
"petal by petal the flower of our heart;",
"the end lost in dream,",
"they float past our view,",
"we only watch their glad, early start."
]
/* 遇到多参数的,需要用偏函数处理成单参数 */
const changeMap = fn => function(arr) { return arr.map(fn) }
/* 首字母大写 */
const fristEmToUpper = changeMap(str => str.charAt(0).toLocaleUpperCase() + str.substring(1))
/* 全部转大写 */
const toUpper = changeMap(str => str.toLocaleUpperCase())
/* 数组转字符串 */
const join = arr => arr.join('\r\n')
// 首字母转大写
const fn1 = compose(join, fristEmToUpper)
/*
Life is a stream
On which we strew
Petal by petal the flower of our heart;
The end lost in dream,
They float past our view,
We only watch their glad, early start.
*/
console.log(fn1(data))
// 全部转大写
const fn2 = compose(join, toUpper)
/*
LIFE IS A STREAM
ON WHICH WE STREW
PETAL BY PETAL THE FLOWER OF OUR HEART;
THE END LOST IN DREAM,
THEY FLOAT PAST OUR VIEW,
WE ONLY WATCH THEIR GLAD, EARLY START.
*/
console.log(fn2(data))
函数组合的数据流是从右至左,因为最右边的函数首先执行,将数据传递给下一个函数以此类推,有人喜欢另一种方式最左侧的先执行,我们可以实现pipe(可称为管道、序列)来实现。它和compose所做的事情一样,只不过交换了数据方向。
其他组合子
-
辅助组合子 无为(nothing)、照旧(identity)、默许(defaultTo)、恒定(always)
-
函数组合子 收缩(gather)、展开(spread)、颠倒(reverse)、左偏(partial)、右偏(partialRight)、柯里化(curry)、弃离(tap)、交替(alt)、补救(tryCatch)、同时(seq)、聚集(converge)、映射(map)、分捡(useWith)、规约(reduce)、组合(compose)
-
谓语组合子 过滤(filter)、分组(group)、排序(sort)
-
其它 组合子变换juxt