函数式编程

拉勾大前端的笔记,仅作为学习记录

课程介绍

  • 为什么学习函数式编程,以及什么是函数编程
  • 函数式编程的特性(纯函数,柯里化,函数组合)
  • 函数式编程的应用场景
  • 函数式编程库 Lodash

为什么学些函数式编程

随着React 的流行备受关注,Vue3.0也开始拥抱函数式,函数式可以抛弃this,打包过程中更好的利用tree shaking 过滤无用代码,方便测试,方便并行处理,有很多库可以帮助我们进行函数开发 Lodash,underscorce,ramda

什么是函数编程

函数式编程是编程范式之一,常说的还有面向对象和面向过程
面向对象:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承、多态演示事物之间的联系
函数式编程: 把现实世界中事物和事物之间的联系抽象成程序世界,是对运算过程的抽象,像纯函数(相同的输入得到相同的输出) eg. y=sin(x-0)

函数式编程--前置知识

  • 函数是一等公民
  • 高阶函数
  • 闭包

函数是一等公民

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数可以作为返回值
    在JavaScript中函数式一个普通的对象 (可以通过 new Function),我们可以把函数存储在变量/数组中,它还可以作为另一个函数的参数和返回值,甚至我们可以在程序运行的时候通过new Function(‘alert(1)’)来构造一个新的函数
把函数赋值给变量
let fn = function(){
  console.log(`hello`)
}
fn()

高阶函数

  • 高阶函数(Higher-order function)
    • 可以把函数作为参数传递给另一个函数
    • 可以把函数作为另一个函数的返回结果

函数作为参数传递给另一个函数

// 模拟forEach
function forEach(arr,fn){
  for( let i = 0; i < arr.length; i++ ){
    fn(arr[i])
  }
}
//模拟filter
function filter(arr,fn){
  const res = []
  for( let i = 0; i < arr.length; i++ ){
    if(fn(arr[i])){
      res.push(arr[i])
    }
  }
  return res
}

函数作为返回值

function makeFn(){
  let msg = `hello function`
  return function(){
    console.log(msg)
  }
}
// 模拟once 使用场景:支付场景
function once(fn){
  const done = false
  return function(){
    if(!done){
      done = true
      return fn.apply(this, arguments)   // 用户调用的时候可能会传递参数,所以把调用当前函数的参数传递给fn
    }
  }
}
let pay = once(function(money){
  console.log(`支付了 ${money} RMB`)
})

使用高阶函数的意义

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

常见的高阶函数

// 模拟map
const map = (arr,fn)=>{
  const results = []
  for(const value of arr ){
    results.push(fn(value))
  }
  return results 
}
// 模拟every
const every = (array, fn) => {
  let result = true
  for(let value of array){
    result = fn(value)
    if(!result){
      break
    }
  }
return result
}
// 模拟some
const some = (array, fn) => {
  let result = false
  for(let value of array){
    result = fn(value)
    if(!result){
      break
    }
  }
return result
}

闭包

闭包的概念

闭包(Closure): 函数和其周围的状态(词法环境)的引用捆绑在一起行程闭包

  • 可以在另一个作用于中调用一个函数的内部函数并访问到改函数的作用域中的成员
function makeFn (){
  let msg =  `Hello`
  return function(){
    console.log(msg)
  }
}
const fn = makeFn()  // 因为外部对内部的函数有引用,所以内部的msg变量不能被释放掉
fn()
- 闭包的本质:函数在执行的时候会被放到一个执行栈上,当函数执行完毕后,会被从执行栈中移出,**但是堆上的作用域成员因为被外部引用不能释放**,因此内部函数依然可以访问外部函数成员
闭包 的案例
// 求平方
function makePower (power){
  return function (number){
    Math.pow(number,power)
  }
}
const power2 = makePower(3) // 求3的平方
// 求员工的工资
function makeSalary (base){
  return function (performance){
    return base + performance
  }
}

纯函数

纯函数的概念

纯函数 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

  • 纯函数就类似于数学中的函数,用来描述输入和输出之间的关系,y=f(x)
  • lodash 是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法
  • 数组中的slice 和 splice 分别是:纯函数和不纯函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作返回数组,会改变原数组
let array = [1,2,3,4,5]
// 纯函数
console.log(array.slice(0,3)) // [1,2,3]
console.log(array.slice(0,3)) // [1,2,3]
// 不纯函数
console.log(array.splice(0,3)) // [1,2,3]
console.log(array.splice(0,3)) // [4,5]
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
  • 我可以把一个函数的执行结果交给另一个函数去处理

Lodash中提供的纯函数

const array = ['jack','lucy','mack','nike']
console.log(_.first(array)) // jack
console.log(_.last(array)) // nike
console.log(_.toUpper(_.first(array))) JACK

纯函数的好处

  • 可缓存
    因为相同的输入始终会有相同的结果,所以可以把纯函数的结果缓存起来
    使用缓存的原因:如果有个函数调用起来特别耗时,并且需要多次调用,可以在第一次调用的时候把结果缓存起来,第二次调用直接返回缓存的结果
// lodash中的记忆函数
const _ = require('lodash')
function getArea(r){
  console.log(r)
  return Math.PI *r *r
}
let getAreaWithMemory =  _.memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 圆的面积
// 圆的面积
// 圆的面积

模拟memoize

function memoize(f){
  const cache = {}
  return function(){
   let key = JSON.stringfy(arguments)
   cache[key] = cache[key] || f.apply(f,arguments) 
    return cache[key]
  }
}
  • 可测试
    因为纯函数始终有输入输出,让测试更方便
  • 并行处理
    • 在多线程环境下并行操作共享的内存数据可能会发生意外情况
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数

纯函数的副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
// 不纯的
let mini = 18
function checkAge(age){
  return age >= mini
}
// 纯的(有硬编码,后续通过柯里化解决)
function checkAge (age){
  let mini = 18
  return age >= mini  
}

副作用让一个函数变得不纯(如上例),纯函数是根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保成输出相同,就会带来副作用
副作用的来源:

  • 配置文件
  • 数据库
  • 获取用户的输入
  • ...
    所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合拓展和可重用性,同时副作用会给程序带来安全应还给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生

柯里化

柯里化概念

  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)
  • 然后返回一个新的函数接受剩余的参数,返回结果
    使用柯里化解决上一个案例中硬编码的问题
// 存在硬编码的函数
function checkAge(age){
  let min = 18
  return age >= mini
}
// 普通的纯函数
function checkAge( min, age){
  return age >= min
}
// 函数的柯里化
function checkAge(min){
  return function (age){
    return age >= mini
  }
}
// ES写法
let checkAge = min => (age=> age >= min)

let checkAge18 = checkAge(18)
checkAge18(20) // true

Lodash中的柯里化函数

_.curry(func)
  • 功能:创建一个函数,该函数接收一个或多个func的参数,如果func所需要的参数都被提供则执行func并返回执行结果,否则继续执行返回该函数并等待接收剩余的参数
  • 参数: 需要柯里化的函数
  • 返回值: 柯里化后的函数
// lodash 中 curry 的基本使用
// 柯里化可以把任意多元的函数转化为一元的函数
const _ = require('lodash')
function getSum(a,b,c){
  return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1,2,3)) // 6 因为传递了func所需的所有参数,func会被执行并返回执行结果
console.log(curried(1)(2,3)) //当传递了一个函数后,会返回一个新的函数并等待接收剩余参数
console.log(curried(1,2)(3)) 

柯里化的案例

// 匹配字符串中的所有空白字符
// 之后还需要提取字符串中的数字
// 面向过程的方式
' '.match(/\s+/g)
' '.match(/\d+/g)
// 纯函数
function match (reg,str) {
  return str.match(reg)
}
// 柯里化处理
const _ = require('lodash')
const match = _.curry(function(reg,str){
  return str.match(reg)
})

const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)
const filter = _.curry((func,array) => array.filter(func))

const findSpace = filter(haveSpace)
findSpace(['hello world','jkl'])

柯里化实现原理

function curry(func){
  return function curriedFn(...args){
    // 判断实参和形参的个数
    if(args.length < func.length){
      return function (){
        return curriedFn(...args.concat(Array.form(arguments)))
      }
    }
    return func(...args)
  }
}

柯里化总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对函数参数的‘缓存’
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转化为一元函数,可以组合使用参数产生强大的功能

函数组合 Compose

纯函数和柯里化很容易写出洋葱代码h(g(f(x)))

  • 获取数组的最后一个元素再转换成大写字母
_.toUpper(_.first(_.reverse(array)))

函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

管道

下面这张图表示程序中使用函数处理数据的过程,给fn函数输入参数a,返回结果b,可以想到a数据通过管道得到了b数据

image.png

当函数fn比较复杂的时候,我们可以把函数fn拆成多个小函数,此时多了中间运算过程产生的m和n

下面这张图可以想象成把fn这个管道拆分成3个管道f1,f2,f3,数据a通过管道f3得到结果m,m再通过管道f2得到结果n,n通过管道f1得到最终的结果b


image.png
fn = compose(f1,f2,f3)
b = fn(a)

函数组合概念

函数组合(compose) 如果一个函数经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数

  • 函数就像是一个数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
  • 函数组合默认是从右到左执行
// 组合函数演示
// 获取数组的最后一元素
function compose(f, g){
    return function(value){
        f(g(value))
    }
}

function reverse(array){
    return array.reverse()
}

function first(array){
    return array[0]
}

const last = compose(first, reverse)
console.log(last([1,2,3,4]))

Lodash中的组合函数

  • lodash中提供组合函数flow()或者flowRight(),他们可以组合多个函数
  • flow()是从左到右运行
  • flowRight()是从右到左运行,使用更多一些
// lodash 中的 _.flowRight()
const _ = require('lodash')
const reverse = array => array.reverse()
const first = array => array[0]
const toUpper = array => array.toUpperCase()

const fn = _.flowRight(toUpper, first, reverse)
console.log(fn(['one, 'two', 'three']))

flowRight的实现原理

function compose(...args){
    return function(value){
        return args.reverse().reduce(function(acc,fn){
            return fn(acc)
        }, value)
    }
}
// es6改写
const compose = (...args) => value => args.reverse().reduce( (acc,fn )=> fn(acc), value)

函数组合

结合律
函数组合要满足结合律

  • 我们既可以把g和h组合,还可以把f和g组合,结果都是一样的
// 结合律
const f = _.flowRight(_.toUpper, _.first, _.reverse)
const f = _.flowRight(_.flowRight(_.toUpper,_.first),_.reverse)
const f = _.flowRight(_.toUpper,_.flowRight(_.first, _.reverse))

调试

// NEVER SAY DIE --> never-say-die
// 思路:通过过滤空格,将字符串转化为数组,把数组的每一项变为小写,通过字符串‘-’分割数组

const split = _.curry((sep,str) => _.split(str,sep))
const map = _.curry((fn,array)=> _.map(array,fn))
const join = _.curry((spe,array)=>_.join(array,spe))

// 调试的时候可以写一个辅助函数,看上一个函数的执行结果,并把执行结果返回给下一个待执行的函数
const log = v => {
    console.log(v)
    return v
}
// 改造log
const trace = _.curry((tag,v)=>{
  console.log(tag,v)
  return v
})

const fn = _.flowRight( join('-'), trace('在map之后打印的'),map(_.toLower),  log, split(' '))
console.log('NEVER SAY DIE')

Lodash中的FP模块

  • lodash的fp模块提供了实用的对函数式编程友好的方法
  • 提供了不可变 已经被柯里化的(auto-curried)并且遵循函数有先(iteratee-first) 数据滞后(data-last)的方法
const _ = require('lodash')
// lodash 模块中数据优先函数置后
_.map(['a','b','c'],_.toUpper)
_.split('hello world', ' ')

const fp = require('lodash/fp')
// fp模块中函数优先,数据置后
fp.map(fp.toUpper,['a','b','c'])
fp.map(fp.toUpper)(['a','b','c'])
fp.split(' ', 'hello world')
fp.split(' ')('hello world')
// 通过fp改造之前的Never SAY DIE -> nerver-say-die 案例
const fp = require('lodash/fp')
const fn = fp.flowRight(fp.join('-'),fp.map(fp.toLower,fp.split(' ')))

lodash 中的map和fp中的map区别

  • lodash中的map后面的function接收三个参数 value:any, index|key, array
  • fp中的map中function只接收一个参数 value:any

Point Free

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

  • 不需要指明处理的数据
  • 只需要合成运算的过程
  • 需要定义一些辅助的基本运算函数
// Hello  World -> hello_world
const fp = require('lodash/fp')
const f = fp.flowRight(fp.replace(/s+/g, '_'), fp.toLower)

案例

// 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(' '))

函子Functor

为什么学习函子

到目前为止已经学习了函数式编程的一些基础,但是我们还没有演示在函数式编程中如何把副作用控制到可控范围内、异常处理、异步操作等

什么是Functor

  • 容器:包含值和值得变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数,对值进行处理(变形关系)
// Functor 函子
class Container { // 创建函子类
  constructor (value) {
    this._value = value // 接收一个value,存储在内部
  }
  map(fn){ // 内部有个map方法接收一个纯函数,处理完数据并返回一个函子对象
    return new Container(fn(this._value))
  }
}
// 调用
let r = new Container(5)
     .map(x => x + 1)
     .map(x => x * x)
console.log(r) // 36

// 对上面的class 类进行改造,封装静态方法of, new Container创建函子对象
class Container {
  static of (value){
    return new Container(value)
  }
  constructor (value) {
    this._value = value // 接收一个value,存储在内部属性_value中
  }
  map(fn){ // 内部有个map方法接收一个纯函数,处理完数据并返回一个函子对象
    return Container.of(fn(this._value))
  }
}

函子总结

  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了map契约的对象
  • 我们可以把函子想象成一个盒子,这个盒子里封装了一个值
  • 想要处理盒子中的值,我们需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数对值进行处理
  • 最终map方法返回一个包含新值得盒子(函子)
// 演示 null undefined 的问题
Container.of(null)
.map(x => x.toUpperCase())
// 传递null会报错,不符合纯函数的特征(相同的输入会有相同的输出)

MayBe函子

  • 我们再编程的过程中可能会处理很多错误,需要对这些错误做相应的处理
  • MayBe 函子的作用就是可以对外部的空值情况做处理(控制副作用在允许的范围)
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(value))
    }
    isNothing(){
        return this._value === null || this._value  === undefined
    }
}
// MayBe 函子的问题
let r = MayBe.of('hello world')
  .map(x => x.toUpperCase())
  .map(null)
  .map(x => x.split(' '))
console.log(r)  // MayBe{_value: null }
// 不知道是什么地方出现了null

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))
    }
}

function parseJSON(str){
    try{
        return Right(JSON.parse(str))
    }catch(e){
        return Left({error:e.message})
    }
}

const r = parseJSON('{name:zs}')
console.log(r) // 走Left

IO函子

  • IO函子中的_value是一个函数,这里是把函数作为值来处理
  • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行)
  • 把不纯的操作交给调用者来处理

const fp = require('lodash/fp')

class IO {
    static of(x){
        return new IO(function(){
            return x
        })
    }
    constructor(fn){
        this._value = fn
    }
    map(fn){
        return new IO(fp.flowRight(fn,this._value))
    }
}

// 调用
let r = IO.of(process).map(p => p.exexPath)
console.log(r._value)

Monad 函子

  • 是可以变扁的Pointed函子,IO(IO(x))
  • 一个函子如果具有join 和 of两个方法,并遵守一些定律就是一个Monad
const fp = require('lodash/fp')

class IO {
    static of(x){
        return new IO(function(){
            return x
        })
    }
    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()
    }
}
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 r = readFileSync('package.json')
        .flatMap(print)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335