函数式编程

1.概念

FP,Functional Programing.

常见的编程范式:

  • 面向过程编程:按照步骤实现想要的功能;
  • 面向对象编程:把现实世界中的事物抽象成程序世界中的类和对象;
  • 函数式编程:把现实世界中的事物和事物之间的联系抽象到程序世界,即对运算过程进行抽象;
    函数式编程中的函数,不是指程序中的函数(方法),而是指数学中的函数(映射)。

Demo: 计算两个数的和

// 面向过程编程
let num1 = 2
let num2 = 3
let sum = num1 + num2 
console.log(sum)

// 函数式编程
function add (a1, a2){
    return a1 + a2
}
let sum = add(2, 3)
console.log(sum)

2.知识回顾:

2.1 函数是一等公民

First-class Function, 头等函数

  • 函数可以存储在变量中
  • 函数可以作为参数
  • 函数可以作为返回值

2.2 高阶函数

Higher-order Function

  • 函数可以作为参数传递给另一个函数,
    如数组的 forEach()、filter()、sort() 方法
  • 也可以作为另一个函数的返回值

Demo: 让某个函数只执行一次

function once(fn){
    let done = false
    return fuction(){
        if(!done){
            fn.apply(this, arguments)
        }
    }
}

let pay = once(function(money){
    console.log(${money}`)
})
console.log(pay)

/*结果:
function(){
    if(!done){
        done = true
        fn.apply(this, arguments)
    }
}
*/

pay(5);
pay(3);
pay(2);

// 结果:¥5
// once函数只执行了一次,pay函数,即执行once时返回的那个function执行了多次

2.3 闭包

Closure:“定义在一个函数内部的函数”。如⬆️的pay()

作用:可以在另一个作用域中调用一个函数(once)的内部函数(pay),并访问到该函数作用域中的成员(done)。


3.纯函数

函数式编程中的函数,指的是 纯函数,类似数学中的函数( y=f(x) )。

相同的输入(参数)永远会得到相同的输出(返回值),而且没有任何可观察的副作用。

  • 可缓存
  • 可测试
  • 并行处理(多线程,ES6 Web Worker)

3.1 副作用

如果函数依赖于外部状态,就无法保证相同输入每次的输出都相同,就会带来副作用。

副作用的来源:

  • 函数外部的变量
  • 配置文件
  • 数据库
  • 获取用户的输入

副作用不可能完全禁止,但要尽量控制在可控范围内。

3.2 Lodash

一个原生的js库,降低了对数组、对象等的使用难度。

// 模拟 lodash 的记忆函数 _.memoize()
// _.memoize()的参数是一个纯函数,返回值是一个带有记忆功能的函数
function memoize(fn){
    let cache = {}
    return function(){
        let key = JSON.stringify(arguments)
        cache[key] = cache[key] || fn.apply(fn, arguments)
        return cache[key]
    }
}

4.柯里化

当一个函数有多个参数的时候,先传递一部分参数,然后返回一个函数接收剩余的参数。

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

// 柯里化
function checkAge(min){
    return function(age){
        return age >= min
    }
}
let checkAge18 = checkAge(18)
let checkAge20 = checkAge(20)

checkAge18(19)
checkAge20(22)

4.1 lodash中的柯里化函数

curry(func),他的作用是,创建一个函数,该函数可以接受一个或多个func的参数,如果func所需的参数都被提供,则直接执行func并返回结果,否则继续返回该函数并等待接收剩余的参数

const _ = require('lodash')

// 要柯里化的函数
function getSum(a, b, c){
    return a+b+c
}

// 柯里化后的函数
let curried = _.curry(getSum)
console.log(curried(1,2,3)) //6
console.log(curried(1))  //getSum(a,b,c)
console.log(curried(1)(2,3)) //6
console.log(curried(1,2))  //getSum(a,b,c)


// 模拟curry函数, 参数是一个纯函数(func),返回值也是一个纯函数
function curry (func){
    return function curriedFn(...args){
        //如果参数少于func所需的所有参数,返回该函数,继续接收剩余的参数
        if(args.length < func.length){
            return function (){
                return curriedFn(...args.contact(Array.from(arguments))
            }
        }
        //如果func所需的参数都被提供,则直接执行func并返回结果
        return func(...args)
    }
}

5.函数组合 Compose

如果一个输入要经过多个函数处理才能得到输出,可以把中间过程的函数合并成一个函数。

  • 函数组合默认从右到左执行
  • 函数组合要满足结合律,可以先结合f1和f2, 也可以先结合f2和f3, 结果是一样的

5.1 lodash中的组合函数

  • flow(),从左到右执行
  • flowRight(),从右到左执行

5.2 模拟flowRight函数

// 参数是多个函数,返回值是组合后的函数
function compose (...args){
    return function(value){
        return args.reverse().reduce(function(acc, currentFn){
            return currentFn(acc)
        }, value)
    }
}

//ES6写法
const compose = (...args) => (value) => args.reverse().reduce((acc, currentFn) => currentFn(acc), value)

5.3 调试组合函数

可以定义一个跟踪函数,来查看组合函数每一步执行的结果。

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

const f = _.flowRight(fn1, trace(fn2), fn2, trace('fn3'), fn3)

5.4 lodash-fp模块

  • 自动柯里化
  • 函数优先,数据滞后(与lodash模块中的方法相反)

5.5 PointFree

一种编码风格,实际上还是函数的组合

  • 不需要指明处理的数据
  • 只需要合成中间的运算过程
  • 需要定义一些辅助的基本运算函数

6.函子 Functor

一个包含值(value)和值的变形关系(map函数)的容器,通过一个对象来实现,该对象具有map方法,map方法运行一个函数(fn)来对值进行处理。

class Container {
    static of(value){  //静态方法
        return new Container(value)
    }

    constructor(value){
        this._value = value  //私有属性
    }
    
    map(fn){
        // return new Container(fn(this._value))
        return Container.of(fn(this._value))
    }
}

let container = new Container(2)

let r1 = new Container(2)
            .map(x => x+1)
            .map(x => x*x)
            
let r2 = Container.of(3)
            .map(x => x+2)
            .map(x => x*x)
            
console.log(typeof(Container))  // function,构造函数
console.log(typeof(container))  // obj: Container{_value: 2}
console.log(r)  //obj: Container{_value: 4}

// 每次调用map都返回一个新的函子(包含新的value),因此可以继续调用它的map函数

6.1 Maybe函子

当外部传入空值时做出处理,但不能确定是哪一步产生的空值

6.2 Either函子

类似 if…else…的处理,需要定义两个类,分别处理正常情况和异常情况。

6.3 IO函子

IO函子传入的值是一个函数

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

Folktale,一个标准的函数式编程库,提供了一些函数式处理的操作,如compose,curry,和一些函子Task, Either, Maybe等

6.4 Task函子

  • task函数用来处理异步操作,
  • 接收一个函数,这个函数的参数是一个对象 resolver,resolver提供两个方法:resolve 和 reject;
  • 返回值是一个Task函子。
function taskTest(){
    return task(resolver => {
        if(false) resolver.reject()
        resolver.resolve()
    })
}

// 执行taskTest函数,返回一个Task函子
taskTest()
    .map()  //在这里对数据进行处理
    .run()  //调用run执行
    .listen({ //在这里传入 resolve 和 reject
        onReject: err => {
            
        },
        onResolve: data => {
            
        }
    })

6.5 Pointed函子

实现了of静态方法的函子。

  • of方法可以避免使用new来创建对象
  • of方法用来把值放到上下文Context,使用map方法来处理

6.6 Monad函子(单子)

可以变扁的Pointed函子 解决函子嵌套问题 IO(IO(x))

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()
    }
}
  • map 方法,对值(this._value)进行处理
  • join 方法,用来把嵌套的函子“拍扁”,把函子的值取出来
  • flatMap(fn),当要合并的的fn返回一个值时调用map, 当要合并的fn返回一个函子时调用flatMap
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值