01.函数式编程

吐槽君 分类:javascript

什么是函数式编程?

函数式编程(Funtional Programming.FP),属于编程范式之一,还有另外的编程范式,面向对象编程,面向过程编程。
 

1.面向对象编程的思维方式:把现实世界中的事物抽象为编程世界中的类和对象,通过封装、继承、多态来显示事物之间的联系。

2.函数式编程的思维方式:将现实世界的事物与事物之间的联系抽象到编程世界,它是对运算过程的抽象。

程序的本质:根据输入通过某种运算获取对应的输出,在开发过程中会涉及很多的输入和输出的函数。

函数式编程中的函数指的不是程序中声明的函数,而是数学学科中的函数,即映射关系

前置知识

1.函数是一等公民

所谓函数是一等公民,是指函数可以存储在变量中,函数可以作为参数,也可以作为返回值,函数在js中就是一个普通的对象,可以赋值给别的变量,也可以通过 new Function 来构建新的函数。

2.高阶函数(Higher-order Function)

高阶函数的定义:1.把函数作为参数传给另一个函数。2.把函数作为一个函数的返回值。满足这两点任意一点这个函数就是高阶函数。

高阶函数的意义:1.帮我们屏蔽细节,只需要关注最终结果。2.用于抽象通用的问题

// 面向过程的方式
// 再没有高阶函数forEach的情况下我们需要自己定义变量跟边界去循环数组
let array = [1, 2, 3, 4]
for (let i = 0; i < array.length; i++) {
    console.log(array[i])
}

// 高阶函数
// 通过forEach这个高阶函数我们只需要传入需要的参数,就可以获取对应的遍历结果。
// 任何数组的遍历都可以通过forEach来完成
function forEach (array, fn) {
    for (let i = 0; i < array.length; i++) {
        fn(array[i])
    }
}
let array = [1, 2, 3, 4]
forEach(array, item => {
    console.log(item)
})
 

3.闭包

定义:在另一个作用域中调用了一个函数返回的内部函数,且内部函数引用了外部函数作用域中的变量,这就形成了闭包。

本质:正常函数执行时会被放到执行栈上执行,执行完毕后函数会被移除同时函数内部作用域的变量一并被释放。而产生的闭包后,函数执行完毕被移除,但是函数内部作用域变量因为仍然被外部引用,所以不会被释放,内部函数依然可以正常访问到该变量。

纯函数

概念:相同的输入始终会得到相同的输出,而且没有任何可观察的副作用

纯函数的好处

可缓存:因为纯函数对相同输入始终有相同的结果,所以可以将纯函数进行缓存。

// loadsh 中的 memoize 方法即可进行缓存
const _ = require('lodash')
function getArea (r) {
    return Math.PI * r * r
}
let getAreaWithMemory = _.memoize(getArea)
console.log(getAreaWithMemory(4))
// 手写 memoize
function memoize(fn){
    let cache = {}
    return function(...args){
        cache[args.toString()] = cache[args.toString()] || fn.apply(fn,args)
        return cache[args.toString()]
    }
}
 

可测试:纯函数让测试更加方便

并行处理:多线程的情况下操作共享的内存数据可能会导致意外情况,而纯函数不依赖与共享的内存数据,所以并行环境下可任意运行纯函数(Web Worker)

副作用

副作用让一个函数变得不纯,纯函数根据相同的输入返回相同的输出,如果函数依赖于外部状态,就无法保证相同的输出,就会带来副作用。

所有的外部交互都可能带来副作用,副作用使得方法的通用性下降,不适合扩展和可重用性,同时副作用会给程序带来安全隐患跟不确定性,但是副作用不可能完全禁止,要尽可能控制他们在可控范围内发生。

柯里化

定义:当一个函数接收多个参数的时候可先传递一部分参数调用它,这部分参数会通过闭包进行保存,然后返回一个接受剩余参数的新函数,当全部参数传递完成才会执行该函数输出结果。

// 柯里化实现
function curry(fn){
    return function newFn(...args){
        if(args.length >= fn.length){
            return fn.apply(fn,args)
        }else{
            return function(){
                return newFn(...args,...arguments)
            }
        }
    }
}
 

好处:1.柯里化可以让我们给一个函数先传递较少的参数,得到一个已经记住某些固定参数的新函数。2.这是对函数参数的一种缓存操作。3.让函数变得更灵活,粒度更小。4.可以把多元函数转换为一元函数,使组合函数产生更强大的功能。

函数组合

洋葱代码,例如获取数组的最后一个元素并转换成大写字母,upper(first(reverse(arr)))
就会这样一层一层嵌套。这时我们可以通过函数组合的方式把细粒度的函数重新组合生成一个新函数

定义:如果一个函数要通过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数。函数就像数据的管道,函数组合就是将多个管道连接起来,让数据经过多个管道形成最终的结果。

函数组合默认执行顺序是从右到左执行。

const _ = require('lodash')
const toUpper = s => s.toUpperCase()
const reverse = arr => arr.reverse()
const first = arr => arr[0]
const f = _.flowRight(toUpper, first, reverse) //lodash提供的组合函数
console.log(f(['one', 'two', 'three']))
 
//组合函数
function flowRight(...fns){
    return function(value){
        return fns.reverse().reduce((acc,fn)=>{
            return fn(acc)
        },value)
    }
}
// es6
const flowRight = (...fns)=>(value)=>fns.reverse().reduce((acc,fn)=>fn(acc),value)
 

函数组合要满足结合律,既可以吧g跟h组合,也可以吧f和g组合,结果一致。

let f = compose(f, g, h)
let associative = compose(compose(f, g), h) == compose(f, compose(g, h))
 

如何调试组合函数

const _ = require('lodash')
const trace = _.curry((type,v)=>{
    console.log(type,v)
    return v
})
const split = _.curry((sep,str)=>_.split(str,sep))
const join = _.curry((sep,arr)=>_.join(arr,sep))
const map = _.curry((fn,arr)=>_.map(arr,fn))
const fn = _.flowRight(join('-'), trace('map 之后'), map(_.toLower),
trace('split 之后'), split(' '))
console.log(fn('NEVER SAY DIE'))
 

lodash的fp模块提供了实用的对函数式编程友好的方法。

提供了不可变的 自动柯里化,函数在前,数据在后的函数。

//lodash模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper)
_.split('Hello World', ' ')
//lodash的fp模块
const fp = require('lodash/fp')
fp.map(fp.toUpper,['a', 'b', 'c'])//函数在前,数据在后
fp.map(fp.toUpper)(['a', 'b', 'c'])//自动柯里化

fp.split('-','Hello-World')
fp.split('-')('Hello-World')
 
//使用fp模块实现上方调试组合函数功能
const fp = require('lodash/fp')
const f = fp.flowRight(fp.join('-'), fp.map(_.toLower), fp.split(' '))
console.log(f('NEVER SAY DIE'))
 

PointFree

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

1.无需指明要处理的数据

2.只需要合成运算过程

3.需要定义一些基本的辅助运算函数

// 使用 Point Free 的模式,把单词中的首字母提取并转换成大写
const fp = require('lodash/fp')
const firstLetterToUpper = fp.flowRight(join('. '),
fp.map(fp.flowRight(fp.first, fp.toUpper)), split(' '))
console.log(firstLetterToUpper('world wild web'))
// => W. W. W
 

Functor(函子)

什么是函子:1.容器,包含值和值的变形关系(变形关系即函数)。2.函子是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。

作用:通过函子可以在函数式编程中将副作用控制在可控范围内,通过函子处理异常,异步操作等。

// 实现一个函子
class Container {
    static of (value){
        return new Container(value)
    }
    constructor(value){
        this._value = value
    }
    map(fn){
        return Container.of(fn(this._value))
    }
}
// 测试
Container.of(3)
.map(x => x + 2)
.map(x => x * x)
 

总结:1.函数式编程的运算不直接操作值,而是由函子完成。2.函子就是一个实现了map契约的对象。3.函子就是一个内部装有一个值的盒子。4.想要处理函子中的值,就需要调用函子的map方法,并传递一个纯函数,由这个纯函数对值进行处理。5.最终map会返回一个装有新值的函子

MayBe 函子

作用:对外部的空值情况做处理(控制副作用在允许的范围内)

class MayBe {
    static of (value){
        return new MayBe(value)
    }
    constructor(value){
        this._value = vlaue
    }
    map(fn){
         return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))   
    }
    isNothing(){
        return this._value === null || this._value === undefined
    }
}
// 传入具体值
MayBe.of('Hello World')
    .map(x => x.toUpperCase())
// 传入 null 的情况
MayBe.of(null)
    .map(x => x.toUpperCase())
// => MayBe { _value: null }
 

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(json) {
    try {
        return Right.of(JSON.parse(json));
    } catch (e) {
        return Left.of({ error: e.message});
    }
}
let r = parseJSON('{ "name": "zs" }').map(x => x.name.toUpperCase())
console.log(r)
 

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) {
        // 把当前的 value 和 传入的 fn 组合成一个新的函数
        return new IO(fp.flowRight(fn, this._value))
    }
}
let io = IO.of(process).map(p => p.execPath)
console.log(io._value())
 

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)
        })
    })
}
// 调用 run 执行
readFile('package.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run().listen({
        onRejected: err => {
            console.log(err)
        },
        onResolved: value => {
            console.log(value)
        }
    })
 

Pointed 函子

实现了of静态方法的函子就是pointed函子,of方法是为了避免使用new来创建对象,也在于通过of方法将值放到上下文中。

Monad 单子

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

const fp = require('lodash/fp')
const fs = require('fs')
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
    })
}
// IO Monad
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 r = readFile('package.json')
.map(fp.toUpper)
.flatMap(print)
.join()
 

回复

我来回复
  • 暂无回复内容