笔记一:函数式编程范式

文章内容输出来源:拉勾教育大前端高薪训练营 和自我总结

学习函数式编程的意义

1.受React的流行而被人们越来越关注(如:React中的高阶组件使用高阶函数实现的,高阶函数就是函数式编程的一个特性(非纯函数),React生态中redux使用了函数式编程的一些思想)
2.Vue 3 越来越偏向函数式编程
3.函数式编程可以抛弃烦人的this
4.在打包过程中可以更好的利用tree shaking过滤掉无用的代码
5.函数式编程方便测试以及并行处理
6.提供许多库(如:lodash,underscore,ramda)帮助我们进行函数式开发

函数式编程

函数式编程(FP)是编程范式(包括面向过程编程、面向对象编程)之一,它是用来描述数据(函数)之间的映射;是对运算过程的抽象,可以让代码无数次重用,亦可让代码更加简洁
函数式编程中的函数指的并不是程序中的函数,而是数学中的函数(即映射关系),它是一个纯函数,即对相同的输入始终得到相同的输出;这些函数都是细粒度的函数,可以把这些函数组合成功能更强大的函数

函数是一等公民(头等函数)

1.函数可以存储在变量
let fn = function(){
    console.log('hello world!!')
}

fn()
2.函数可以作为参数

好处:它可以让这个函数变得更灵活,而且调用函数时不用考虑函数内部实现的细节,且函数的名字具有实际意义

//高阶函数-函数作为参数

//forEach-遍历函数
function forEach(array,fn) {
    for(let i = 0; i < array.length; i++){
        fn(array[i])
    }
}

//filter-过滤函数
function filter(array, fn){
    let results = []
    for(let i = 0; i < array.length; i++){
        if(fn(array[i])){
            results.push(array[i])
        }
    }
    return results
}

//测试数组
let arr = [1, 4, 6, 7, 11]

//测试
forEach(arr,function (item) {
    console.log(item)
})

let r = filter(arr, function (item) {
    return item % 2 === 0
})
console.log(r)   //[ 4,6 ]
3.函数可以作为返回值
//高阶函数-函数作为返回值
/**让一个函数生成一个函数**/

function makeFn() {
    let msg = 'hello function'
    return function () {
        console.log(msg)
    }
}

const fn = makeFn()
fn()  //hello function

makeFn()()  //hello function


/**应用**/
//once--函数只执行一次
function once(fn) {
    let done = false
    return function () {
        if(!done){
            done = true
            return fn.apply(this, arguments)
        }
    }
}

let pay = once(function (money) {
    console.log(`支付了:${money} RMB`)
})

pay(3) //支付了:3 RMB
pay(3)
pay(3)
pay(3)

高阶函数(Higher-order function)

高阶函数:函数可以作为参数传递给另一个函数、函数也可以作为另一个函数的返回结果(代码实现在上面
高阶函数的意义:它是用来抽象通用的问题,抽象可以帮我们屏蔽细节,只需要关注于我们的目标

常用的高阶函数

forEach / map / filter / every / some / find / finfIndex / reduce / sort / ...
(以上是数组的一些方法,都需要函数作为参数)

// 模拟常用的高阶函数:map、every、some
//函数作为参数可以让函数变得更灵活

//map

const map = (array, fn) => {
    let results = []
    for(let value of array){
        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
}

//测试数组
let arr = [1, 2, 3, 4]

//测试
let a = map(arr,v => v * v)
 console.log(arr)  //[ 1, 4, 9, 16 ]

let r = every(arr,v => v > 0)
 console.log(r)  //true

let s = some(arr, v => v >9)
console.log(s)  //false

闭包(Closure)

闭包是指可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员,它是由函数和周围的状态(词法环境)的引用捆绑在一起形成的,它的好处是延长了外部函数的内部变量的作用域范围
闭包的本质:函数在执行的时候会放到一个执行栈上,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用而不能释放,因此内部函数依然可以访问外部函数的成员

闭包-案例
// Math.pow(4, 2)
// Math.pow(5, 2)

function makePower(power) {
    return function (number) {
        return Math.pow(number, power)
    }
}

// 求平方和三次方
let power2 = makePower(2)
let power3 = makePower(3)

// 测试
console.log(power2(4))  // 16
console.log(power2(5))  // 25
console.log(power3(4))  // 64

注:闭包发生是在内部函数被调用的时候发生的

纯函数

纯函数:相同的输入永远得到相同的输出,而且没有任何可观察的副作用;纯函数类似于数学中的函数

// 纯函数和不纯函数
// slice /splice

// 测试数组
let array = [1, 2, 3, 4, 5, 6]

// 纯函数
console.log(array.slice(0, 3))  // [ 1, 2, 3 ]
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, 6 ]
console.log(array.splice(0, 3 ))  // []

// 纯函数
function getSum(n1, n2) {
    return n1 + n2
}

console.log(getSum(3, 4))  // 7
console.log(getSum(3, 4))  // 7
console.log(getSum(3, 4))  // 7

函数式编程不会保留计算中间的结果,所以变量是不可变的(无状态的)
可以把一个函数的执行的结果交给另一个函数去处理(将细粒度的函数传给那些函数库)

lodash纯函数库

// 演示lodash
// first / last / toUpper / reverse / each / includes / find / findIndex
/**
 * 安装lodash
 * 1.先生成一个package.json文件 --- npm init -y
 * 2.安装 npm i lodash
 * **/

// 引入lodash
const _ = require('lodash')


//测试数组
const array = ['jack', 'tom', 'lucy', 'kate']

//测试
console.log(_.first(array))  // jack
console.log(_.last(array))  //kate
console.log(_.toUpper(_.last(array)))  // KATE

/*注意:这里reverse内部调用的即是数组中的reverse,数组中的reverse方法会改变原数组,它不是一个纯函数*/
console.log(_.reverse(array))  // [ 'kate', 'lucy', 'tom', 'jack' ]

const r =_.each(array,(item, index) => {
    console.log(item, index)
})
console.log(r)
// kate 0
// lucy 1
// tom 2
// jack 3
纯函数缓存案例
// 记忆函数
const _ = require('lodash')

function getArea(r) {
    console.log(r)
    return Math.PI * r * r
}

//模拟 memoize 实现
function memoize(fn) {
    let cache = {}
    return function () {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn, arguments)
        return cache[key]
    }
}

//测试
let getAreaWithMemory = memoize(getArea)
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
console.log(getAreaWithMemory(4))
// 4
// 50.26548245743669
// 50.26548245743669
// 50.26548245743669

纯函数的好处

  • 可缓存
    • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
  • 可测试
    • 纯函数让测试更加方便(纯函数始终有输出和输出,可以断点查看)
  • 并行处理
    • 在多线程环境下并行操作共享内存数据很可能会出现意外情况(操作全局变量情况,多个线程操作同一个变量)
    • 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意u运行纯函数(ES6以后新增了Web Worker)

纯函数副作用

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

// 不纯的,函数的返回值依赖外部的变量
let mini = 18
function checkAge (age) {
  return age >= mini
}

// 纯的(有硬编码,后续可以通过柯里化解决)
function checkAge2 (age) {
  let mini = 18
  return age >= mini
}

副作用会让一个函数百年的不纯(如上面例子),纯函数是由相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证相同的输出,那么就会带来副作用
副作用的主要来源:

  • 配置文件
  • 数据库
  • 获取用户的输入(账号密码等...)
  • ......
    所有的外部交互都有可能产生副作用,副作用也使得方法通用性下降不适合扩张和可重用性,同时副作用会给程序带来安全隐患,从而给程序带来不确定性,但是副作用不可能完全禁止,尽可能控制他们在可控范围内发生

柯里化

柯里化使用:

  • 当一个函数有多个参数的时候,先传递一部分参数给它(这部分参数在以后永远不变)
  • 然后返回一个新的函数,用来接收剩余的参数,并处理后返回结果

使用柯里化解决上面硬编码的问题

// 柯里化演示

/*
function checkAge(age) {
    let min = 18
    return age >= 18
}*/

//普通的纯函数
function  checkAge(min, age) {
    return age >= min
}

console.log(checkAge(18,20))  //true
console.log(checkAge(18,24))  //true
console.log(checkAge(20,24))  //true

// 函数的柯里化
function checkAgeAnother(min) {
    return function (age) {
        return age >= min
    }
}

let checkAge18 = checkAgeAnother(18)
let checkAge20 = checkAgeAnother(20)

console.log(checkAge18(17))  //false
console.log(checkAge18(24))  //true

// ES6写法
let checkAgeEs6 = min => (age => age >= min)

let checkAgeEs618 = checkAgeEs6(18)

console.log(checkAgeEs618(20)  //true

Lodash中的柯里化函数

_curry(func)

  • 功能:创建一个函数,该函数接收一个或多个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
console.log(curried(1)(2,3))  // 6
console.log(curried(1,2)(3))  // 6
案列
// 柯里化案例
// ''.match(/\s+/g)
// ''.match(/\d+/g)

const _ = require('lodash')

const match = _.curry(function (reg, str) {
    return str.match(reg)
})

const haveSpace = match(/\s+/g)
const haveNumber = match(/\d+/g)

console.log(haveSpace('hello world')) // [ ' ' ]
console.log(haveSpace('helloworld')) // null

console.log(haveNumber('123abc')) // [ '123' ]
console.log(haveNumber('abc'))  //null

const filter = _.curry(function (fn, array) {
    return array.filter(fn)
})

const filterEs6 = _.curry((fn,array) => array.filter(fn))

console.log(filter(haveSpace,['John Connor', 'John_Donne'])) //[ 'John Connor' ]
console.log(filterEs6(haveSpace,['John Connor', 'John_Donne'])) //[ 'John Connor' ]
柯里化原理模拟
// 模拟实现 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
console.log(curried(1)(2,3))  // 6
console.log(curried(1,2)(3))  // 6


function curry(fn) {
    return function curriedFn(...args) {
        if(args.length < fn.length) {
            return function () {
                return curriedFn(...args.concat(Array.from(arguments)))
                //或借鉴别人的写法 return curriedFn(..args, ...arguments)
            }
        }else{
            return fn(...args)
        }
    }
}
柯里化总结
  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数
  • 这是一种对参数的‘缓存’
  • 让函数变得更灵活,让函数的粒度更小
  • 可以把多元函数转换成一元函数,可以使用组合函数生成功能更强大的函数

函数组合(Compose)

一个函数如果要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数;函数组合可以让我们把细粒度的函数重新组合生成一个新的函数

  • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
  • 函数组合默认是从右到左执行
// 函数组合演示

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, 2, 3]))  // 3

Lodash中的组合函数

  • lodash中组合函数flow()或则flowRight(),他们都可以组合多个函数
  • flow()是从左到右运行
  • flowRight() 是从右到左执行,使用的更多一些
// lodash 中的函数组合方法 _.flowRight()
const _ = require('lodash')

const reverse = arr => arr.reverse()

const first = arr => arr[0]

const toUpper = s => s.toUpperCase()

const f = _.flowRight( toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))  //THREE
flowRight组合函数原理模拟
// flowRight组合函数原理模拟

const reverse = arr => arr.reverse()

const first = arr => arr[0]

const toUpper = s => s.toUpperCase()

/*function  compose(...args) {
    return function(value){
     return args.reverse().reduce(function (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(['one', 'two', 'three']))  //THREE
函数的组合要满足结合律
// 组合函数结合律测试
const _ = require('lodash')

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

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

const f2 = _.flowRight(_.toUpper,_.flowRight( _.first, _.reverse))
console.log(f2(['one', 'two', 'three']))  //THREE
函数组合调试方法
// 函数组合 调试
// NEVER SAY DIE  ---> never-say-die

const _ = require('lodash')

const log = v => {
    console.log(v)
    return v
}

const trace = _.curry((tag, v) => {
    console.log(tag, v)
    return v
})
// _.split()
const split = _.curry((sep, str) => _.split(str, sep))

//_.toLower()

// _.join()
const join = _.curry((sep,str) => _.join(str, sep))

// _.map()

const map = _.curry((fn, array) => _.map(array, fn))
const f = _.flowRight( join('-'), trace('map 之后'),  map(_.toLower), trace('map 之前'), split(' '))

console.log(f('NEVER SAY DIE'))  // never-say-die

Lodash-fp模块

  • lodash的fp模块提供了实用的对函数式编程友好的方法, 函数优先,数据在后
  • 提供了不可变auto-curried iteratee-first data-last的方法
//lodash 的 fp 模块
const fp = require('lodash/fp')

const f = fp.flowRight(fp.join('-'), fp.map(fp.toLower), fp.split(' '))

console.log(f('NEVER SAY DIE'))  // never-say-die
lodash与lodash-fp中map的区别
//lodash与lodash-fp中map的区别
const _ = require('lodash')

// lodash中的map中的函数的参数有三个:(item, index, array)
console.log(_.map(['23', '5', '10'],parseInt))  // [ 23, NaN, 2 ]
// parseInt('23', 0, array) 第二个参数是0,则是10进制
// parseInt('5', 1, array) 第二个参数是1,不合法,输出NaN
// parseInt('10', 2, array) 第二个参数是2,表示2进制,输出2

// lodash-fp中的map中的函数的参数有1个:(item)
const fp = require('lodash/fp')
console.log(fp.map(parseInt, ['23', '5', '10']))  //[ 23, 5, 10 ]

Pointfree模式

Point Free是一种编程的风格,它的具体实现是函数组合,它更抽象些;我们可以把数据处理的过程定义为与数据无关的合成运算,把需要用到代表数据的那个参数,只要把简单的运算步骤合成在一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
// point free实现即为函数的组合
// Hello World => hello_world

const fp = require('lodash/fp')

const f = fp.flowRight(fp.replace(/\s+/g,'_'),fp.toLower)
console.log(f('Hello World'))  // hello_world
point free案例
//把一个字符串的首字母转换成大写,使用.作为分隔符
// world wild web =>W.W.W

const fp = require('lodash/fp')

// 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'))  //W.W.W

函子(Functor)

什么是函子
  • 容器:包含值和值的变形关系(这个变形关系就是函数)
  • 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)
//Functor 函子
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) // Container { _value: 36 }

更换为静态方法创建对象

//Functor 函子

class Container {
    static of (value) {
        return new Container(value)
    }
    constructor(value) {
        this._value = value
    }

    map(fn){
        return Container.of(fn(this._value))
    }
}

// 返回的是一个新的函子
let r = Container.of(5)
            .map(x => x + 2)
            .map(x => x * x)
            .map(x => console.log(x)) // 49
console.log(r)  // Container { _value: 49 }
函子总结
  • 函数式编程的运算不直接操作值,而是由函子完成
  • 函子就是一个实现了map契约的对对象
  • 我们可以把函子想象成一个盒子,这个盒子封装了一个值
  • 想要处理盒子中的值,我么需要给盒子的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
  • 最终map方法返回一个包含心智的盒子(函子)
MayBe 函子

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(this._value))
    }
    isNothing(){
        return this._value === null || this._value === undefined
    }
}

let r = MayBe.of('Hello World')
    .map(x => x.toUpperCase())

console.log(r)  //MayBe { _value: 'Hello World' }

let y = MayBe.of(null)
    .map(x => x.toUpperCase())

console.log(y)  //MayBe { _value: null }

let z = MayBe.of('hello world')
    .map(x => x.toUpperCase())
    .map(x => null)
    .map(x => x.split(' '))
console.log(z) //MayBe { _value: null } //缺陷:无法知道null是哪里发生的
Either 函子
  • Either两者中的任何一个,类似于if...else...的处理
  • 异常会让函数变得不纯,Either函子可以用来做异常处理
// 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 l1 = Left.of(12).map(x => x + 2)

console.log(r1)  //Right { _value: 14 }
console.log(l1)  //Left { _value: 12 }

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

}

let m = parseJson('{name: zs}')
let n = parseJson('{"name": "zs"}')
let z = parseJson('{"name": "zs"}').map(x => x.name.toUpperCase())

console.log(m);  //Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
console.log(n);  //Right { _value: { name: 'zs' } }
console.log(z);  //Right { _value: 'ZS' }
IO 函子
  • IO函子中的_value是一个函数,这里是把函数作为值来处理
  • IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
  • 把不纯的操作交给调用者处理
// IO 函子
const fp = require('lodash/fp')

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(p => p.execPath)
console.log(r)  // IO { _value: [Function] }
console.log(r._value()) // F:\developmentEnvironment\nodejs\node.exe
Folktale 函子
Task异步执行
  • 异步任务的实现过于复杂,我们使用folktale中的Task来演示
  • folktale一个标准的函数式编辑库
    • 和lodash、ramda不同的是,他没有提供很多功能函数
    • 只提供了一些函数式处理的操作,例如:compose、curry等,一些函子Task、Either、MayBe等
// folktale 中的 compose、 curry
const {compose, curry} = require('folktale/core/lambda')
const {toUpper, first} = require('lodash/fp')

let f = curry(2, (x, y) => x + y)

console.log(f(1,2))  // 3
console.log(f(1)(2))  // 3

// folktale中的compose相当于lodash中的flowRight
let g = compose(toUpper, first)
console.log(g(['one','two']))  //ONE

folktale(2.3.2)2.x中的Task和1.0中的Task区别很大,1.0这种的用法更接近我们现在演示的函子,下面例子使用2.3来演示

// Task 处理异步任务
const fs = require('fs')
const {task} = require('folktale/concurrency/task')
const {split, find} = require('lodash/fp')

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

readFile('package.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run()
    .listen({
        onRejected: err => {
            console.log(err)
        },
        onResolved:value => {
            console.log(value)  //  "version": "1.0.0",
        }
    })
Pointed函子
  • Pointed函子是实现了of静态方法的函子
  • of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文Context(把值放到容器中,使用map来处理值)
class Container {
  static of (value) {
    return new Container(value)
  }
  constructor(value) {
    this._value = value
  }
  map (fn) {
    return Container.of(fn(this._value))
  }
}
Monad函子
  • Monad函子是可以变扁的Pointed函子,IO(IO(x))
  • 一个函子如果由join和of两个方法并遵守一些定了就是一个Monad
const fp = require('lodash')
const fs = require('fs')
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()
  }

  // 当fn返回一个函子的时候,用flatMap拍平
  flatMap (fn) {
    return this.map(fn).join()
  }
}

const readFile = function (filename) {
  return new IO(function () {
    return fs.readFileSync(filename, 'utf-8')
  })
}

const print = function (x) {
  return new IO(function () {
    console.log(x)
    return x
  })
}

// const cat = fp.flowRight(print, readFile)
// // IO(IO(x))
// // const r = cat('package.json')._value() // IO { _value: [Function (anonymous)] }
// const r = cat('package.json')._value()._value()

const r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()

console.log(r)

你可能感兴趣的:(笔记一:函数式编程范式)