javascript函数式编程基础随笔
- 2020 年 11 月 16 日
- 笔记
- javascript 函数式编程
JavaScript 作为一种典型的多范式编程语言,这两年随着React\vue的火热,函数式编程的概念也开始流行起来,lodashJS、folktale等多种开源库都使用了函数式的特性。
一.认识函数式编程
程序的本质是:根据输入通过某种运算得到输出
函数式编程(Functional programming)是一种编程思想/范式 ,其核心思想是将运算过程抽象成函数(指数学的函数而不是程序的方法或函数即纯函数),也就是面向函数编程,描述函数/数据 之间的映射,做到最大程度的复用; 学习函数式编程真正的意义在于让你认识到一种用函数的角度去抽象问题的思路。如果你手中只有一个锤子,那你看什么都像钉子。
没有最好的,只有最适合的
函数式编程起源是一门叫做范畴论(Category Theory)的数学分支。理解函数式编程的关键,就是理解范畴论。它是一门很复杂的数学,认为世界上所有的概念体系,都可以抽象成一个个的”范畴”(category)。
-
- 所有成员是一个集合
- 变形关系是函数
引用维基百科
“范畴就是使用箭头连接的物体。”(In mathematics, a category is an algebraic structure that comprises “objects” that are linked by “arrows”. )
也就是说,彼此之间存在某种关系的概念、事物、对象等等,都构成”范畴”。只要能找出它们之间的关系,就能定义一个”范畴”,代码中称之为容器
-
- 值(value)
- 值的变形关系(函数)
class container { constructor(v) { this._v = v; } addOne(x) { return x + 1; } }
container是一个类,也是一个容器,里面包含一个值(this._v)和一种变形关系(addOne)。这里的范畴,就是所有彼此之间相差1的数字。
本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。
二. 函数相关知识
1. 函数是一等公民 (MON first class function)
函数可以存储变量 即函数表达式
var fn = function (){};
函数可以是参数
var fn = function (fn1){…}
fn(function(){…})
函数可以是返回值
function fn(){return function(){…}}
2.高阶函数(higher order function),用于抽象通用问题;
把函数作为参数传递给另一个函数
function forEach(arr ,fn){ for(var i = 0; i < arr.length; i++){ fn1(arr[i], i); } } forEach([1,2,3], function(el, i){...})
函数作为另一个函数的返回结果
function fn(){ return function(){ console.log(1) } } fn()();
3.闭包(Closure),函数按值传递的引用都是闭包
function fn(){ var a = 1; return function(){ console.log(a) } } var r = fn(); r(); r(); function Once(fn, context) { return function () { fn.apply(context || this, arguments); runOnce = null; } }
三.函数式编程基础
1.纯函数:相同输入永远得到相同输出,并没有任何副作用,其是描述输入输出的关系;
面向对象语言的问题是,它们永远都要随身携带那些隐式的环境。你只需要一个香蕉,但却得到一个拿着香蕉的大猩猩…以及整个丛林
by Erlang 作者:Joe Armstrong
所以纯函数的好处:
可缓存(Cacheable)
可测试 (Testable)
并行处理 (Parallel Code)
可移植性/自文档化(Portable / Self-Documenting)
function fn(a){ return function(b){ return a + b; } } var n = fn(20) n(20)// 40 n(20)// 40 //纯函数可缓存 function fn(a){ let obj = {}; return function(b){ let key = b.toString(); return obj[key]? obj[key]:(obj[key]=a + b,console.log('相同输入执行一次'),obj[key]); } } var n = fn(20) n(20)// 40 n(20)// 40 n(30)// 50 n(30)// 50
纯函数的副作用会让纯函数变得不纯,副作用产生来源来自外部交互/数据;副作用不能完全禁止;
副作用是在计算的过程中,系统状态的一种变化,或者与外部世界进行的可观察的交互。
只要是跟函数外部环境发生的交互就都是副作用,这一点可能会让你怀疑无副作用编程的可行性。
函数式编程的哲学就是假定副作用是造成不正当行为的主要原因。
Shared mutable state is the root of all evil
共享可变状态是万恶之源
by Pete Hunt
//不纯的函数 保留计算中间的结果 function fn(a){ var res = 0; return function(b){ res += a + b; return res; } } var n = fn(20) n(20)// 40 n(20)// 80 n(20)// 120 // 不纯的函数 引用外部数据 var a = 2; function fn(n){ return n > a; // 这里 a 就是副作用来源 } // 修改即 function fn(n){ var a = 2; return n > a; // 虽然改成纯函数了但引发了硬编码问题 } // 再次修改 此处用高阶函数 function fn(base){ return function(n){ return base > n } }
2.珂里化:当一个函数有多个参数时可以先传递一部分参数(这部分参数不再改变)并返回新的函数用于接收剩余参数 并返回计算结果 柯里化强调的是生成单元函数,部分函数应用强调的固定任意元参数,而我们平时生活中常用的其实是部分函数应用,这样的好处是可以固定参数,降低函数通用性,提高函数的适合用性。
珂里化函数也是高阶函数
降维处理 转化为一元函数 粒度小更灵活
function test(a,b,c){ console.log(a,b,c) } function curry(fn){ return function fn1(...a){ if(arguments.length >= fn.length)return fn(...a) return function(...b){ return fn1(...a.concat(b)) } } } var s = curry(test); s(1)(2)(3)// 纯珂里化 s(1)(2,4)// 部分函数应用 这里也是 高级珂里化 s(1,4)(2)// 部分函数应用 这里也是 高级珂里化
不难发现以上的curry函数其实不是一个真正的珂里化函数,如果你用过lodash.Ramda 这些库中实现的 curry 函数的行为会发现他们是一样的。其实,这些库中的 curry 函数都做了很多优化导致这些库中实现的柯里化其实不是纯粹的柯里化,我们可以把他们理解为“高级柯里化”。实现可以根据你输入的参数个数,返回一个柯里化函数/结果值。参数个数满足了函数条件,则返回值。这样可以解决一个问题,就是如果一个函数是多输入,就可以避免使用 s(1)(2)(3) 这种形式传参了。
我们可以用高级柯里化去实现部分函数应用,但是柯里化不等于部分函数应用。
3.函数组合(compose),解决洋葱代码 fn(fn1(fn2(fn3(x)))); 默认从右到左执行
function fn(a){ return a + 2 } function fn1(a){ return a * 2 } function fn2(a){ return a * a } function compose(...a){ a.reverse(); return function(v){ return f(a, v)() } function f(arr, v){ var i = 0; var d = v; return function c(){ if(i >= arr.length)return d; return (d = arr[i](d),i++, c()); } } } var r = compose(fn1, fn2, fn) r(2) // 64 r(4) // 144 r(3) // 100 console.log(r(2),r(3),r(4)) // 函数组合要满足结合律即数学中的结合律 var r = compose(fn2, compose(fn1, fn)) r(2) // 64 r(4) // 144 r(3) // 100 var r = compose(compose(fn2, fn1), fn) r(2) // 64 r(4) // 144 r(3) // 100 // 函数组合调试 借用辅助函数即可 var log = function(tar, v){ console.log(tar, v) return v } var r = compose(fn1,log('fn2函数处理结果:'), fn2, log('fn函数处理结果:'), fn) r(5)
4.pointfree 是一种编程风格 组合函数就是pointfree风格
不需要指明处理的数据
只需要合成运算过程
需要定义一些辅助的基本函数
函数式编程就是把运算过程抽象成函数 而pointfree是在此基础上在合成新的函数
// 非pointfree function fn(v){ return v.split('').reverse().join('') } // pointfree function split(v, reg){ return v.split(reg) } function reverse(v){ return v.reverse() } function join(v, reg){ return v.join(reg) } var f1 = curry(split) // 珂里化函数, var f2 = curry(join) var r = compose(f2(''), reverse, f1('')) // 函数组合
4.函子(functor),函子是函数式编程里面最重要的数据类型,也是基本的运算单位和功能单位。 函子首先是一容器,包含了值和变形关系。比较特殊的是,它的变形关系可以依次作用于每一个值,将当前容器变形成另一个容器。就是将一个容器转成另一个容器;
函子是一个特殊的对象,其对外提供一个map方法对值进行操作 该方法接受一个纯函数作为参数
可以链式调用
class container{ constructor(v){ this._v = v } map(fn){ return new container(fn(this._v)) } } var r = new container(2).map(function(v){ return v + 2 }).map(function(v){ return v * v }); console.log(r) // 此时 r是一个函子对象 {_v: 16} 函子将数据包装在内部不对外公布 若对值进行操作/使用在map中完成; // 将new 也封装在函子内部 class container{ static of(v){ return new container(v) } constructor(v){ this._v = v } map(fn){ return container.of(fn(this._v)) } } var r = container.of(2).map(function(v){ return v + 2 }).map(function(v){ return v * v }); console.log(r) // {_v: 16} // 函子 null || undefined 问题 var r = container.of(null) .map(function(v){ return v + 2 }) .map(function(v){ return v * v }); console.log(r) // error ---- Uncaught SyntaxError: Unexpected token 'null'
以上代码最后 null 引发了副作用 使得map的参数函数变成非纯函数, 纯函数是相同的输入始终有相同的输出; 而这里直接抛出异常了.
MayBe 函子 处理异常空值 上面问题 由MayBe函子来捕获处理
class MayBe{ static of(v){ return new MayBe(v) } constructor(v){ this._v = v; } map(fn){ return this.isNull()? MayBe.of(this._v):MayBe.of(fn(this._v)) } isNull(){ return this._v === null || this._v === undefined; } } var r = MayBe.of(null) .map(function(v){ return v + 2 }); console.log(r) // {_v: null} // 此时maybe函子虽然解决了异常空值但延伸了另一个问题 var r = MayBe.of(null) .map(v => v+2) .map(function(v){ return null; }) .map(function(v){ return v + 2 }) console.log(r) //{_v: null}
MayBe函子虽然处理了空值异常,但捕获不到异常具体信息,无法定位哪个函子出错;
Either 函子 类似if…else..,下面示例将处理捕获异常信息,及定位出错函子;
class left { static of(v){ return new left(v) } constructor(v) { this._v = v } map(fn){ return this } } class right { static of(v){ return new right(v) } constructor(v) { this._v = v } map(fn){ return right.of(fn(this._v)) } } function test(v){ try{ return right.of(JSON.parse(v)) }catch(e){ return left.of({ error: e.message}) } } var r = test('{a": "1"}') .map(x => x.a+2) console.log(r) // {_v: {error: "Unexpected token a in JSON at position 1"}}
IO函子: input output 惰性执行不纯的操作,使当前函数变为纯函数,其实就是通过compose组合函数根据map调用次数依次组合成一个纯函数返回
class IO { static of(v){ return new IO(function(){ return v }) } constructor(fn) { this._v = fn } map(fn){ return new IO(compose(fn, this._v)) } } var r = IO.of(location).map(l => l.href) console.log(r) // {_v: function} // _v就是组合函数生成的新的纯函数 需要调用的话 可以直接 r._v(); 但是这样就不符合属性私有化了 更重要的是如果函子嵌套就要不停的._v() var io1 = function(x){ return new IO(function(){ return x * 2 }) } var io2 = function(x){ return new IO(function(){ return x }) } var comIo = compose(io2, io1); var r = comIo(2)._v()._v()
monad 函子 解决函子嵌套问题
如果一个函子具有join,map两个方法并遵守一些定律 就是一个monad
class IO { static of(v){ return new IO(function(){ return v }) } constructor(fn) { this._v = fn } map(fn){ return new IO(compose(fn, this._v)) } join(){ return this._v() } } var r = io1(2) .map(io2) .join() .join() console.log(r) // 4 // 虽然封装进了join方法但还要去一次调用,还可以在改进下 class IO { static of(v){ return new IO(function(){ return v }) } constructor(fn) { this._v = fn } map(fn){ return new IO(compose(fn, this._v)) } join(){ return this._v() } flatMao(fn){ return this.map(fn).join() } } var r = io1(2) .flatMao(io2) .join() console.log(r)// 4 // flatMao 是处理返回值是函子的情况 而map 则是处理数据 var r = io1(2) .map(x => x + 2) .flatMao(io2) .map(v => v * v) .join() console.log(r) // 36
pointed 函子 指实现了of静态方法的函子 以上函子均属于pointed函子;
函数式编程往往会导致函数过度包装,影响性能
为减少函数副作用也会使资源占用不能及时释放 使得 Garbage Collection 压力增加
函数式编程尾递归使用频繁,不利于编译器优化