函数式编程

函数式编程

函数式编程中的函数,不是指的程序中的函数(方法),而是数学中的映射关系 y=f(x)
函数式编程用来描述数据(函数)之间的映射

函数是一等公民

  1. 函数可以储存在变量中
  2. 函数可以作为参数
  3. 函数可以作为返回值

高阶函数

高阶函数(higher-order-function)

  1. 可以把函数作为参数传递给另一个函数
  2. 可以把函数作为另一个函数的返回结果

作用:

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

函数作为参数

function forEach (array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i])
    }
}
let arr = [1,23,5]
forEach(arr, function (item) {console.log(item)})


function filte (arr, fn) {
    var res = []
    for (let i = 0; i < array.length; i++) {
        const element = array[i];
        if(fn(element)) {
            res.push(element)
        }
    }
    return res
}

函数作为返回值

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

let pay = once(function(moeny) {
    console.log(moeny)
})

pay(5)

闭包Closure

闭包:函数和其周围的状态的引用捆绑在一起,形成的闭包。可以在另一个作用域中,调用函数的内部函数并访问到该函数的作用域成员。
闭包的本质:当函数执行的时候,函数会被防到一个执行栈上去,当函数执行完毕后会从执行栈中移除,但是堆上的作用域成员,因为外部引用不会被释放,所以内部函数依旧可以访问外部函数的成员。

function makeFn () {
    let msg = 'hello'
    return function () {
        console.log(msg)
    }
}
let a = makeFn()
a() //此时函数里的msg变量并不会被释放掉

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

let pay = once(function(moeny) {
    console.log(moeny)
})
//只会执行一次
pay(5)
pay(5)

应用1: 求次方

//为不同的数求它的几次方
function makePower (number) {
    return function (power) {
        return Math.pow(number, power)
    }
}
//最开始确认到底要求哪个数的次方
let num = makePower(5);
//然后再求这个数的几次方   5的二次方,三次方
num(2);
num(3);
//为不同级别的员工求他们的总工资(基本工资+绩效工资)
 function makeSalary (base) {
     return function (base, performance) {
         return base + performance
     }
 }
 //确定不通风级别的员工
 let salary1 = makeSalary(1200)
 let salary2 = makeSalary(1300)
//确定此员工的绩效工资
 salary1(200)
 salary2(300)

纯函数

纯函数:使用相同的输入,永远会得到相同的输出,而且没有可观察的副作用
lodash: 是一个纯函数的功能库,提供了对数组、字符串、数字、函数等操作的一些方法。

//slice  splice
let arr = [1, 2, 3, 5]
//纯函数:相同输入,相同的输出
arr.slice(0, 2)
arr.slice(0, 2)
arr.slice(0, 2)
//不纯函数:相同的输入,不同的输出
arr.splice(0, 1)
arr.splice(0, 1)
arr.splice(0, 1)

纯函数的好处

1.由于纯函数是相同的输入始终都有相同输出,所以可以将输出的结果缓存起来。
2.好测试
3.并行处理

function getArea (r) {
    console.log('once')
    return Math.PI * r * r
}
//缓存了getArea这个函数,等于只需要从上面拿一次,接下来的调用都从缓存里拿
//模拟memoize方法
function memoize (fn) {
    //将函数的参数作为key,将函数返回值作为value
    let cache = {}
    return function () {
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn, arguments)
        return cache[key]
    }
}

const getAreaWithMemoize = memoize(getArea)
getAreaWithMemoize(3) //once

副作用

副作用让一个函数变的不纯,纯函数是相同的输入有着相同的输出结果,如果函数依赖于外部的状态就无法保证相同的输出,就会带来副作用。
副作用的来源

  1. 全局变量
  2. 配置文件
  3. 数据库
  4. 获取用户输入

所有外部交互都有可能带来副作用,副作用也使得代码通用性下降使得不可扩展和可重用性,同时还有可能给程序带来安全隐患和不确定性,但副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化

  1. 当一个函数有多个参数的时候,我们可以先传递一部分参数去调用它(这部分参数以后永远不变)
  2. 然后返回一个新的函数用来接收剩参数,返回结果
// 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, 10))
console.log(checkAge(18, 23))
console.log(checkAge(13, 20))

// function checkAge2 (min) {
//     return function (age) {
//         return age >= min
//     }
// }
let checkAge2 = (min) => ((age) => age >= min)
let min_18 = checkAge2(18)
min_18(24)
min_18(22)

lodash中的柯里化函数

_.curry(func)

  1. 功能:创建一个函数,该函数接收一个或者多个fnc参数,如果fnc的参数都被提供执行则执行func,并返回执行结果。否则继续返回该函数并等待接收剩余参数。
  2. 参数:需要柯里化的函数
  3. 返回值:柯里化后的函数
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)(4)) //7
console.log(curried(1)(4, 5)) //10

案例:

// ''.match(/\s+/g)
// ''.match(/\d+/g)
/**
 * 
 * @param {*} res 正则表达式
 * @param {string} str 字符串
 */
// function match (res, str) {
//     return str.match(res)
// }
const match = _.curry(function (res, str) {
    return str.match(res)
})
const haveSpace = match(/\s+/g)
console.log(haveSpace('hello world'))

const filter = _.curry(function (func, arr) {
    return arr.filter(func)
})
//用来过滤含有空格的字符
console.log(filter(haveSpace, ['h a', 'aa']))
const findSpace = filter(haveSpace)
console.log(findSpace(['h a', 'aa']))

模拟lodash中curry函数

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

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)(4)) //7
console.log(curried(1)(4, 5)) //10

函数组合compose

如果一个函数要通过多个函数(每个函数的参数只能是一个)的处理才能得到最终值,这个时候可以把中间过程函数合并成一个函数。
函数就像是数据的管道,函数组合就是把这些管道接起来,让数据穿过多个管道并形成最终结果。
函数组合默认是从右到左执行
函数组合要满足结合律

//函数组合
function compose (f, g) {
    return function (value) {
        //函数组合默认从右到左执行
        return f(g(value))
    }
}

//反转数组
function reverse (arr) {
    return arr.reverse()
}

//获取数组中第一个值
function first (arr) {
    return arr[0]
}

const last = compose(first, reverse)

console.log(last([1,3,5]))

lodash中的组合函数

lodash中的组合函数flow()或者flowRight()
flow()从左往右执行
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']))

模拟lodash中的flowRight

//模拟flowRight
// function componse (...args) {
//     return function (value) {
//         return args.reverse().reduce(function (acc, fn) {
//             return fn(acc)
//         }, value)
//     }
// }

const componse = (...args) => value => args.reverse().reduce((acc, fn) => fn(acc), value)

函数组合调试

//NEVER SAY DIE -->  never-say-die
const _ = require('lodash')
//先转换为数组,并且把数组中的数据转为小写,最后用split链接起来
//由于函数组合好每一个函数都只有一个参数,所以将split跟join柯里化
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, arr) => _.join(arr, sep))

//定于debug函数
const v = (value) => {
    console.log(value)
    return value
}

const map = _.curry((func, arr) => _.map(arr, func))

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

console.log(f('NEVER SAY DIE'))

Lodash-fp模块

//lodash-fp
const _ = require('lodash')

//lodash中map方法是数据优先,函数之后
// _.map(['23', '8', '10'], parseInt)

const fp = require('lodash/fp')
//fp方法中map是函数优先
fp.map(parseInt, ['23', '8', '10'])

Point Free

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

  1. 不需要指明处理的数据
  2. 只需要合成运算过程
  3. 需要定义一些辅助的基本运算函数
//point free
//Hello World --> hello_world
//转换小写 --> 字符串替换

const fp = require('lodash/fp')

const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)

f('Hello World')

//把一个字符串中的首字母提取,并转换为大写,使用.做分隔符
//world wild web --> W.W.W
//用空格切割字符串 --> 数组中的每一个元素转化大写 --> 把数组中每项的第一个字符取出 --> join链接

// const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.first), fp.map(fp.toUpper), fp.split(' '))
//中间两个参数都是map,对数组进行了两次遍历,所以可以进行优化一下
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方法可以运行一个函数对值进行处理(变形关系)
//函子

// 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 + 2)

class Container {
    constructor (value) {
        this._value = value
    }

    static of (value) {
        return new Container(value)
    }

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

let r = Container.of(5)
            .map(x => x * x)

总结:

  1. 函数式的编程运算不直接操作值,而是由函子完成
  2. 函子就是一个实现了map契约的对象
  3. 可以把函子想象成一个盒子,这个盒子封装了一个值
  4. 要想处理盒子里的值,我们需要给盒子的map方法传递一个处理值的纯函数,由这个函数来对值进行处理
  5. 最终map方法返回一个包含新值的盒子

Maybe函子

编程中可能会遇到很多错误,需要对这些错误进行相应的处理,maybe函子的作用就是可以对外部的空值情况做处理(控制副作用在允许范围)

//maybe函子

class MayBe {
    constructor (value) {
        this._value = value
    }

    static of (value) {
        return new MayBe(value)
    }

    map (fn) {
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }

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

// let r = MayBe.of('hello')
//             .map(x => x.toUpperCase())
// let r = MayBe.of(undefined)
//             .map(x => x.toUpperCase())
            
// console.log(r)

//无法确定到底是哪次调用出现了问题
let r = MayBe.of('hello')
            .map(x => x.toUpperCase())
            .map(x => null)
            .map(x => x.split(' '))
console.log(r) //MayBe { _value: null }

Either函子

Either两者中任何一个,有点类似于if…else的处理。
异常会让函数变的不纯,Either可以对异常进行处理。

//either函子

class Left {
    constructor (value) {
        this._value = value
    }

    static of (value) {
        return new Left(value)
    }

    map (fn) {
        return this
    }
}

class Right {
    constructor (value) {
        this._value = value
    }

    static of (value) {
        return new Right(value)
    }

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

// const r1 = Right.of(12).map(x => x + 2)
// const r2 = Left.of(12).map(x => x + 2)

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

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

let r = pareJSON('{ name: zx}')
console.log(r) //Left { _value: { error: 'Unexpected token n in JSON at position 2' } }

let r1 = pareJSON('{ "name": "zx"}')
            .map(x => x.name.toUpperCase())
console.log(r1) //Right { _value: 'ZX' }

IO函子

IO:input-output输入输出
IO函子中的_value是一个函数,这里是把函数作为值来处理。
IO函子可以把不纯的动作储存到_value中延迟执行这个不纯的操作(惰性执行),保证当前的操作纯。
把不纯的操作交给调用者来处理。

//IO函子
const fp = require('lodash/fp')

class IO {
    constructor (fn) {
        this._value = fn
    }

    static of (value) {
        return new IO(function () {
            return value
        })
    }

    map (fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}

const r = IO.of(process).map(x => x.execPath)
console.log(r) //IO { _value: [Function] }
console.log(r._value()) ///usr/local/bin/node

folktale

Task异步执行
folktale是一个标准函数式编程库,和lodash、ramda不同的是,他没有提供很多的功能函数。
只提供了一些基本的函数式操作,如compose,curry等,一些函子Task,Either,Maybe等。

const { compose, curry } = require('folktale/core/lambda')
const { toUpper, first } = require('lodash/fp')

//第一个参数用来指明第二个函数的参数有几个
let f = curry(2, (x, y) => {
    return x + y
})

console.log(f(1, 2))

let c = compose(toUpper, first)
console.log(f(['one, two']))
//task处理异步任务
const { task } = require('folktale/concurrency/task')
const fs = require('fs')
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方法里处理返回结果
    .map(find(x => x.includes('version')))
    .run()
    .listen({
        onRejected: err => {console.log(err)},
        onResolved: data=> {console.log(data)} // "version": "1.0.0",
    })

Pointed函子

Pointed函子是实现了of方法的函子。
of方法是为了避免new来创建对象,更深层的意思是of方法用来把值放在上下文Context(把值放入容器中,使用map来处理值)。

//IO函子
const fp = require('lodash/fp')
const fs = require('fs')

class IO {
    constructor (fn) {
        this._value = fn
    }

    static of (value) {
        return new IO(function () {
            return value
        })
    }

    map (fn) {
        return new IO(fp.flowRight(fn, this._value))
    }
}

// const r = IO.of(process).map(x => x.execPath)
// console.log(r) //IO { _value: [Function] }
// console.log(r._value()) ///usr/local/bin/node

//读取文件操作,由于调用外部资源会有副作用,所以用IO函子处理,这样返回的内容保证是纯的,因为返回的内容是一个IO对象
function readFile (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8')
    })
}

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

let cat = fp.flowRight(print, readFile)
//IO(IO(x))
let r = cat('package.json')._value()._value() //注意这里就很不方便,嵌套几层IO就要写几个_value
console.log(r)

monad函子

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

//IO函子
const fp = require('lodash/fp')
const fs = require('fs')

class IO {
    constructor (fn) {
        this._value = fn
    }

    static of (value) {
        return new IO(function () {
            return value
        })
    }

    map (fn) {
        return new IO(fp.flowRight(fn, this._value))
    }

    join () {
        return this._value()
    }

    flatMap (fn) {
        return this.map(fn).join()
    }
}

//读取文件操作,由于读取文件操作会有副作用,所以用IO函子处理
function readFile (filename) {
    return new IO(function () {
        return fs.readFileSync(filename, 'utf-8')
    })
}

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

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

你可能感兴趣的:(学习笔记)