函数式编程

190 阅读8分钟

函数式编程

函数式编程总结

  1. 认识函数式编程
  2. 函数复习 (1)函数是一等公民 (2)高阶函数 (3)闭包
  3. 函数式编程基础 (1)lodash (2)纯函数 (3)柯里化 (4)管道 (5)函数组合
  4. 函子 (1)Functor (2)MayBe (3)Either (4)IO (5)Task(folktale) (6)Monad

认识函数式编程

  1. 随着React的流行收到越来愈多的关注, React高阶组件使用函数式编程实现
  2. Vue 3 开始拥抱函数式编程
  3. 函数式编程可以抛弃this
  4. 打包过程中可以更好的利用tree-shaking过滤无用代码
  5. 方便测试, 方便处理
  6. 更多生态库进行函数式开发: lodash、 underscore、ramda

函数式编程概念

  1. FP是编程范式之一 (面向过程、面向对象编程)
  2. 面向对象编程的思维方式: 把现实世界中的事物抽象成程序世界中的类和对象, 通过封装、继承、多态来演示事物事件的联系
  3. 函数式编程的思维方式: 把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象) 程序本质: 根据输入通过某种运算获得相应的输出 函数式编程中的函数是数学中的函数即映射关系 相同的输入始终得到相同的输出(纯函数) 函数式编程用来描述数据(函数)之间的映射

函数复习

函数是一等公民 First-class Function

  1. 函数是一个普通对象: 可以存储到变量/数组中, 可以作为另一个函数的参数和返回值, 可以通过new Function()构造一个新函数 ****MDN中关于函数是一等公民的解释只有三个: (1)可以存储于变量中(2)可以做为参数(3)可以作为返回值 ****函数可以递归调用---不是函数是一等公民的佐证
  2. 函数是一等公民是学习高阶函数、柯里化的基础

高阶函数 Higher-order function

  1. 函数可以作为参数传递给另一个参数 function forEach (array, fn){ for(let i = 0;i < array.length; i++){ fn(array[i]) } } let arr = [1, 3, 4, 9, 10] forEach(arr, function(item){ console.log(item) })
  2. 函数可以作为一个函数的返回值 function makeFn(){ let msg = 'Hello' return function(){ console.log(msg) } } const fn = makeFn() fn() makeFn()()

高阶函数的意义

  1. 抽象可以帮我们屏蔽细节, 只需要关注于我们的目标
  2. 高阶函数是用来抽象通用的问题

常用的高阶函数 forEach map filter every some find/findIndex reduce等

// map const map = (array, fn) => { let results = [] for(let value of array){ results.push(fn(value)) } return results } let arr = [1,3, 4, 5] arr = map(arr, item => item * item) console.log(arr) //every const every = (array, fn) => { let result = true for(let value of array){ result = fn(value) if(!result){ break } } return result } let arr = [1, 3, 5, 5,6, 20] let r = every(arr, v => v > 10) console.log(r) //some const some = (array, fn) => { let result = false for(let value of array){ result = fn(value) if(result){ break } } return result } let arr = [1, 3, 5, 7] let r = some(arr, v => v % 2 === 0) console.log(r)

闭包

  1. 闭包: 函数和其周围的状态(词法环境)的饮用捆绑在一起形成闭包; 可以在另一个作用域中调用一个函数的内部函数并访问到赶海书的作用域中的成员
  2. 闭包的本质: 函数在执行的时候会放到一个执行栈上, 当函数执行完毕之后会从执行栈上移除, 但是堆上的作用域成员因为被外部引用不能释放, 因此内部函数依然可以访问外部函数的成员 //案例一 function forEach (array, fn){ for(let i = 0;i < array.length; i++){ fn(array[i]) } } let arr = [1, 3, 4, 9, 10] forEach(arr, function(item){ console.log(item) }) //案例二 function makeFn(){ let msg = 'Hello' return function(){ console.log(msg) } } const fn = makeFn() fn() makeFn()()

函数式编程基础

纯函数

  1. 相同的输入永远会得到相同的输出, 而且没有任何可观察的副作用 副作用: 函数依赖于外部的转台就无法保证输出相同, 就会带来副作用 副作用来源: (1) 配置文件 (2) 数据库 (3) 获取用户的输入 --- 所有的外部交互都有可能代理副作用, 副作用也使得方法通用性下降, 不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性, 但是副作用不可能完全禁止, 仅能能控制他们在可控范围内发生 --- 副作用让函数变得不纯
  2. 函数式编程不会保留计算中间的结果, 所以变量时不可变的(无状态的)
  3. 我们可以吧艺哥函数的执行结果交给另一个函数去处理 // slice splice slice 纯函数 let array = [1, 3, 5,6,4] console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) console.log(array.slice(0, 3)) splice 不纯函数 console.log(array.splice(0, 3)) console.log(array.splice(0, 3)) console.log(array.splice(0, 3)) // 求和纯函数 function getSum (n1, n2){ return n1 + n2 } console.log(getSum(2, 4)) console.log(getSum(2, 4)) console.log(getSum(2, 4))
  4. 纯函数的好处: (1) 可缓存 (2) 可测试 (3) 并行处理 function getArea (r){ console.log(r, 'r') return Math.PI * r * r } let getAreaWithMemery = _.memoize(getArea) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4)) // 模拟 memoize方法实现 function memoize (fn){ let cache = {} return function(){ console.log(arguments[0], fn) let key = JSON.stringify(arguments) console.log(key, fn.apply(fn, arguments), 'key') cache[key] = cache[key] || fn.apply(fn, arguments) return cache[key] } } let getAreaWithMemery = memoize(getArea) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4)) console.log(getAreaWithMemery(4))

柯里化

  1. 使用柯里化解决硬编码的问题 ***当一个函数有多个参数的时候先传递一部分参数调用他(这部分参数以后永远不变) ***然后返回一个新的函数接受剩余的参数返回结果 //柯里化演示 function checkAge(age){ let min = 18 return age >= min } //普通的纯函数 function checkAge(min, age){ return age >= min } console.log(checkAge(18, 20)) console.log(checkAge(18, 20)) console.log(checkAge(22, 20)) console.log(checkAge(24, 20)) // 函数柯里化 function checkAge(min){ return function(age){ return age >= min } } // ES6 let checkAge = min => (age => age >= min) let checkAge18 = checkAge(18) console.log(checkAge18(20)) console.log(checkAge18(22)) console.log(checkAge18(16))
  2. lodash中的柯里化 _.curry 功能: 创建一个函数,该函数接受一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行的结果. 否则继续返回该函数并等待接收剩余的参数. function getSum(a, b, c){ return a + b + c } const curried = _.curry(getSum) console.log(curried(2, 3, 5)) console.log(curried(2)(3, 5)) console.log(curried(2)(3)(2)) console.log(curried(2, 3)(2)) //柯里化案例 ''.match(/\s+/g) ''.match(/\d+/g) function match (reg, str){ return str.match(reg) } const match = _.curry(function (reg, str){ return str.match(reg) }) const haveSpace = match(/\s+/g) const haveNumber = match(/\d+/g) console.log(haveSpace('hello')) console.log(haveNumber('123456asldk')) const filter1 = _.curry(function(fn, array){ return array.filter(fn) }) console.log(filter1(['ldkk llll', 'liuchao'], haveSpace)) const findSpace = filter1(haveSpace) console.log(findSpace(['ldkk llll', 'liuchao']))
  3. 柯里化实现原理 模拟实现lodash.curry方法 function curry(fn){ return function curriedFn(...args){ //判断实参和行参的个数 if(args.length < fn.length){ return function(){ // return curriedFn(...args.concat([...arguments])) return curriedFn(...args.concat(Array.from(arguments))) } } return fn(...args) } } let curried1 = curry(getSum) console.log(curried1(1, 2, 3)) console.log(curried1(1)(2)(3)) console.log(curried1(1)(2, 3)) console.log(curried1(1, 2)(3))
  4. 函数柯里化总结
    1. 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
    2. 这是一种对函数参数的‘缓存’
    3. 让函数更灵活, 粒度变得更小
    4. 可以把多元函数转换成一元函数, 可以组合使用函数产生强大的功能

管道 (自我理解: gulp链式实现)

函数就是数据的管道, 函数组合就是把这些管道连接起来, 然数据穿过多个管道形成最终结果

函数的组合 Compose

  1. 纯函数和柯里化很容易写出洋葱代码(f(g(k(x)))) function compose(f, g){ return function(value){ return f(g(value)) } } function reverse (array){ return array.reverse() } function first (array){ return array[0] } const last = compose(first, reverse) console.log(last([1, 3, 5, 2]))
  2. 函数组合可以让我们把细粒度的函数重新组合生成一个新函数 函数组合(compose): 如果一个函数需要经多多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数 **函数组合默认是从右到左执行 **函数组合可以让代码最大程度的复用 // 组合函数如何调试 // 实现: NEVER SAY DIE -> never-say-die

const split = _.curry((sep, str) => _.split(str, sep))

const join = _.curry((sep, array) => _.join(array, sep))

const log = v => { console.log(v) return v } const trace = _.curry((tag, v) => { console.log(tag, v) return v }) const map = _.curry((fn, array) => _.map(array, fn)) // const f = .flowRight(join('-'), log, map(.toLower), log, split(' ')) const f = .flowRight(join('-'), trace('map 后'), map(.toLower), trace('map 前'), split(' '))

console.log(f('NEVER SAY DIE')) 3. lodash中的组合函数 // lodash中的组合函数 flow (从左向右执行) flowRight (从右向左执行) const reverse = arr => arr.reverse() const first = arr => arr[0] const toUpper = s => s.toUpperCase() const f = _.flowRight(toUpper, first, reverse) const f = compose(toUpper, first, reverse) console.log(f(['aldj', 'kdkdkd'])) // 组合函数的实现原理 模拟lodash.flowRight function compose (...args){ return function(value){ return args.reverse().reduce((acc, fn) => { return fn(acc) }, value) } } // 箭头函数实现 const compose = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value) const f = compose(toUpper, first, reverse) console.log(f(['aldj', 'kdkdkd'])) 4. 组合函数的实现原理 结合律(函数组合要满足的特点)------我们可以把g和h组合, 还可以把f和g组合, 结果都是一样的 const h = .flowRight(.toUpper, _.first, _.reverse) const g = .flowRight(.toUpper, .flowRight(.first, .reverse)) const h = .flowRight(.flowRight(.toUpper, _.first), _.reverse) console.log(h(['aldj', 'kdkdkd'])) console.log(g(['aldj', 'kdkdkd'])) 5. lodash中的FP模块 (1)lodash中的FP模块提供了实用的函数式编程友好的方法 (2)提供了不可变的auto-curried iteratee-first data-last 的方法( 将函数柯里化 方法前置 数据后置) const fp_f = _fp.flowRight(_fp.join('-'), _fp.map(_fp.toLower), fp.split(' ')) console.log(fp_f('NEVER SAY DIE')) //lodash中的FP模块 和非FP模块 map方法的区别 console.log(.map(['23', '3', '10'], parseInt)) parseInt('23', 0, array) parseInt('8', 1, array) parseInt('10', 2, array) console.log(_fp.map(parseInt, ['23', '3', '10']))

PrintFree 编程风格

-- 我们可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数, 只要把简单的元素按步骤合成到一起, 在使用这种模式之前我们需要定义一些辅助的基本运算函数
    ** 不需要知名处理的数据
    ** 只需要合成运算过程
    ** 需要定义一些辅助的基本运算函数

// PrintFree 模式 // Hello World -> hello_world const f = _fp.flowRight(fp.replace(/\s+/g, ''), _fp.toLower) console.log(f('Hello World'))

// PrintFree 案例 // world wild web -> W. W. W. const firstLetterToUpper = _fp.flowRight(_fp.join('. '), _fp.map(_fp.first), _fp.map(_fp.toUpper), _fp.split(' ')) const firstLetterToUpper = _fp.flowRight(_fp.join('. '), _fp.map(_fp.flowRight(_fp.first, _fp.toUpper)), _fp.split(' ')) console.log(firstLetterToUpper('world wild web'))

函子

Functor

  1. 容器: 包含值和值的变形(这个变形关系就是函数)
  2. 函子: 是一个特殊的容器, 通过一个普通的对象来实现, 该对象具有map方法, map方法可以运行一个函数对值进行处理(变形关系)
  3. 总结: 1. 函数式编程的运算不直接操作值, 而是由函子完成, 2. 函子就是一个实现了map契约的对象 3. 我们可以把函子想像成一个盒子, 这个盒子里封装了一个值 4. 行要处理盒子中的值, 我们需要给盒子的方法传递艺哥处理值的函数(纯函数), 有这个函数来对值进行处理 5. 最终map方法返回一个包含新值的盒子(函子)
  4. 问题: null undefined问题 class Container { constructor (value){ this._value = value } map(fn){ return new Container(fn(this._value)) } } let r = new Container(5).map(x => x + 1).map(x => x * x) console.log(r) class Container { static of (value){ return new Container(value) } constructor (value){ this._value = value } map(fn){ return new Container(fn(this._value)) } } let r = Container.of(5).map(x => x + 1).map(x => x * x) console.log(r) // 问题 (传入值是null undefined) Container.of(null).map(x => x.toUpperCase())

MayBe函子

MayBe函子的作用就是可以对外部的空值情况做处理(空值副作用在允许的范围)
问题: 多次调用 不知道哪次调用出现开始出现null

class MayBe { static of(value){ return new MayBe(value) } constructor(value){ this._value = value } map(fn){ return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value)) }

isNothing(){
    return this._value === null || this._value === undefined
}

} let r = MayBe.of('Hello kitty').map(x => x.toUpperCase()) console.log(r) let r = MayBe.of(null).map(x => x.toUpperCase()) console.log(r) let r = MayBe.of(null).map(x => x.toUpperCase()).map(x => null).map(x => x.split(' ')) console.log(r)

Either函子

Either两者中的任何一个, 类似于if else 的处理
异常会让函数变的不纯, Either函子可以用来做异常处理

class Left { static of(value){ return new Left(value) } constructor(value){ this._value = value } map(fn){ return this } } class Right { static of(value){ return new Right(value) } constructor(value){ this._value = value } map(fn){ return Right.of(fn(this._value)) } } let r1 = Right.of(12).map(x => x + 2) let r2 = Left.of(12).map(x => x + 2) console.log(r1) console.log(r2) function parseJSON (str) { try{ return Right.of(JSON.parse(str)) } catch(e){ return Left.of({error: e.message}) } } let r = parseJSON('{name: zs}') console.log(r) let r = parseJSON('{"name": "zs"}').map(x => x.name.toUpperCase()) console.log(r)

IO函子

IO函子的_value是一个函数,这里是把函数作为值来处理
IO函子可以把不纯的动作存储到_value中, 延迟执行这个不纯的操作(惰性执行), 包装但前的操作纯
把不纯的操作交给调用者来处理
作用: 把传入函子的不纯操作延后, 交给调用者执行, 不再函子中执行, 保证函子的纯

class IO { static of(value){ return new IO(function(){ return value }) } constructor(fn){ this._value = fn } map(fn){ return new IO(_fp.flowRight(fn, this._value)) } } let r = IO.of(process).map(v => v.execPath) console.log(r) console.log(r._value())

IO函子的问题 多次调用_value才能拿到目标数据

// IO函子的问题 let readFile = function(filename){ return new IO(function(){ return fs.readFileSync(filename, 'utf-8') }) } let print = function(x){ return new IO(function(){ console.log(x) return x }) } // let cat = _fp.flowRight(print, readFile) //IO(IO(x)) // let r = cat('package.json') // console.log(r) // let r = cat('package.json')._value()._value() // console.log(r) let r = readFile('package.json') // .map(x => x.toUpperCase()) .map(_fp.toUpper) .flatMap(print).join() console.log(r)

Monad函子

Monad函子是可以变扁的函子 IO(IO(x))
一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad

class IO { static of(value){ return new IO(function(){ return value }) } constructor(fn){ this._value = fn } map(fn){ return new IO(_fp.flowRight(fn, this._value)) } join(){ return this._value() } flatMap(fn){ return this.map(fn).join() } }

Pointed函子

Pointed孩子是实现的of静态方法的函子
of方法是为了避免使用new来创建对象, 更深层的含义是of方法用来把值放到上下文Context(把值放到容器中, 使用map来处理)

folktale 不用于lodash ramda的库, 只提供了一些函数式操作, 例如: compose、curry等 一些Task、Either、MayBe函子

Task异步执行

// folktale库(2.3.2) const { compose, curry } = require('folktale/core/lambda')

// let f = curry(2, (x, y) => { // return x + y // }) // console.log(f(1, 3)) // console.log(f(1)(3))

// let f = compose(_fp.toUpper, _fp.first) // console.log(f(['one', 'tow']))

// Task函子 处理异步任务

// const { task } = require('folktale/concurrency/task')

// function readFile (filename){ // return task( resolver => { // fs.readFile(filename, 'utf-8', (err, data) => { // if(err) resolver.reject(err) // resolver.resolve(data) // }) // }) // }

// let r = readFile('package.json').map(_fp.split('\n')).map(_fp.find(x => x.includes('version'))).run().listen({ // onRejected: err => { // console.log(err) // }, // onResolved: value => { // console.log(value) // } // })