函数式编程学习心得

·  阅读 305

函数式编程概念

什么函数式编程?

函数式编程(Functional Programming, FP),FP 是编程范式之一,我们常听说的编程范式还有面向过程编程、面向对象编程。

面向对象编程(object oriented programming)(OOP):

把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系

优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统 更加灵活、更加易于维护

缺点:性能比面向过程低

面向过程编程(procedure oriented Programming)(POP):

分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。

缺点:没有面向对象易维护、易复用、易扩展

函数式编程(functional programming)(FP):

把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象) 函数式编程中的函数指的的不是程序中的函数(方法),而是数学中的函数即映射关系 函数式编程用来描述数据(函数)之间的映射

优点:

  1. 更好的管理状态:因为它的宗旨是无状态,或者说更少的状态,能最大化的减少这些未知、优化代码、减少出错情况
  2. 更简单的复用:固定输入->固定输出,没有其他外部变量影响,并且无副作用。这样代码复用时,完全不需要考虑它的内部实现和外部影响
  3. 更优雅的组合:往大的说,网页是由各个组件组成的。往小的说,一个函数也可能是由多个小函数组成的。更强的复用性,带来更强大的组合性
  4. 隐性好处。减少代码量,提高维护性

缺点:性能较差,会大量使用闭包

函数式编程基础

为什么说函数是一等公民

概念理解:可以把函数看做成普通对象对待(变量可以做的他也能做) 即:

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

高阶函数的定义:1、可以将函数作为参数传入 2、可以将函数作为返回值

闭包 (重点)

定义:闭包是一种机制,可以保存或保护数据,正是如此也会增加内存开销。
该机制的产生:当函数可以记住并访问所在的此法作用域时就产生了闭包。即使函数在当前词法作用域之外执行。

通俗的说:可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员

例子1:

    function foo(){
        let a = 0
        function bar(){
            console.log(a)
        }
        return bar
    }
    var fn = foo()
    fn() //0 这就是闭包效果
复制代码

不难看出 bar()的此法作用域可以访问foo()的内部作用域,我们将bar所引用的函数对象本身当做返回值。当foo()执行后foo内部函数 bar 赋值给变量给 fn 并调用执行。执行fn() 就等于 执行bar(),即它在自己定义的词法作用域以外的地方执行了。

通常在foo函数执行之后,整个内部作用域就会销毁:引擎的垃圾回收器会释放不在使用的内存空间。但是因为闭包的存在,垃圾回收被阻止了,使得foo内部作用域依然存在,即bar()本身在使用,并且foo的内部作用域会一直存活,以供bar()在之后任何时间进行引用。

例2:函数只执行一次

//once
function once (fn) {
    let done = false 
    console.log(done) //done在执行一次后就会变成false,并保留下来
    return function () {
       if (!done) {
          done = true 
          return fn.apply(this, arguments)
       }
    }
}

let pay = once(function(a){
    console.log('支付了' + a + 'RMB')
})

pay(5) //支付了5RMB
pay(6) //无(不执行)
pay(7) //无(不执行)

//第一次执行pay时会将 done 置为true并且保留下来,之后每次执行pay 都会因为done为false 而无返回
复制代码

闭包的本质:函数在执行的时候会放到一个执行栈上当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员

纯函数 - 相同的输入有相同的输出

概念:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用(通过外部改变变量影响内部结果) 优势:

  1. 可缓存结果
  2. 测试更方便
  3. 并行处理下,在多线程环境下并行操作共享的内存数据可能会出现意外情况,纯函数不需要访问共享的内存数据,所以在并行环境下任意运行纯函数。

例1:缓存结果函数

function memorize(fn){
    let cache = {}
    return function(){
        let arg_str = JSON.stringfy(arguments)
        cache[arg_str] = cache[arg_str] || fn.apply(fn,argumens)
        return cache[arg_str]
    }
}
复制代码

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

  1. 配置文件
  2. 数据库
  3. 获取用户交互
  4. ...

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

例2:

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

柯里化 - 多元函数单一化

概念:当一个函数有多个参数的时候,先传递一部分参数调用它(这部分参数以后永远不变),然后一个新的函数接收剩余的参数,返回结果。

例子1:

实现一个add方法,使计算结果能够满足如下预期:
console.log(add(1)(2)(3)) // 6;
console.log(add(1, 2, 3)(4)) // 10;
console.log(add(1)(2)(3)(4)(5)) // 15;
复制代码
function add(...args){
    if (!args.length) return
    //第一次调用
    let total = args.reduce((prev,cur) =>{
            return prev + cur
    },0)
    //后续调用 因为闭包 total会一直保存
    function sum(...nextArgs){
        let all = nextArgs.reduce((prev,cur) =>{
                return prev + cur
        },0)

        total += all
        return sum
    }
    // log一个函数的引用是会用函数的.toString() 来得到函数的字符串定义
    sum.toString = () => total
    //输出total
    sum.valueOf = () => total
    // console.log(total)
    return sum
}

console.log(add(1,2,3)) // 6
let a = add(2,2,3) 
console.log(a) // 7
复制代码

例子2:手写柯里化实现函数

    function curry(fn){
        return function curried(...args){
            //实参 与 形参比较
            if(args.length < fn.length){
                //结合剩余参数 返回函数
                return function(...restArgs){
                    return curried(...args.concat(...restArgs))
                }
            }
            return fn(...args)
        }
    }
复制代码

函数的组合

概念:如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数,一般默认是从右往左执行

例子1:使用lodash 中的flowRight() 函数

    //引入lodash函数
    const_=require('lodash')
    //字符串编程大写
    consttoUpper=s=>s.toUpperCase()
    //数组取反
    constreverse=arr=>arr.reverse()
    //取首位数据
    constfirst=arr=>arr[0]
    //首先将数组取反 --> 获取数据第一位元素 --> 将元素变为大写  备注:前者函数执行结果是后者的传参数据
    constf=_.flowRight(toUpper, first, reverse)
    console.log(f(['one', 'two', 'three'])) //THREE
复制代码

例子2:手写组合函数 compose

function compose(...funs){
    return function(value){
        funs.reverse().reduce(function(acc,fn) =>{
            return(fn(acc))
        },value)
    }
}

//ES6版本
const compose = (...funs) => value => funs.reverse().reduce((acc,fn) => fn(acc),value) 
复制代码

函数组合需要满足结合律(associativity):
const f = compose(a,b,c) = compose(a,compose(b,c)) = compose(compose(a,b),c)

PointFree - 一种编程风格

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

  1. 不需要指明处理的数据
  2. 只需要合成运算过程
  3. 需要定义一些辅助的基本运算函数

例子:

现在要从中找出所有的女性用户并返回这些女性用户的电话号码

//数据格式 
var users = [
  {name:"john lennon", sex: "male", phone:"123"},
  {name:"prince", sex: "male", phone:"234"},
  {name:"rihanna", sex:"female", phone:"345"},
  {name:"taylor swift", sex:"female", phone:"456"}
]

//常规解决方法 编写一个getFemalPhone函数
function getFemalPhone(users){
    var phones = []
    for(let item of users){
        if(item.sex === 'female'){
            phones.push(item.phone)
        }
    }
    return phones
}
//调用
getFemalPhone(users)

//PointFree风格 将需求进行功能拆分 分别得出 查询女性getFemal() 和获取号码getPhone()两个辅助函数
getFemal(users){
    return users.filter(u => u.sex === 'female')
}
getPhone(users){
    retrun users.map(u => u.phone)
}
//定义
const getFemalPhone = compose(getPhone,getFemal)
//调用
getFemalPhone(users)
复制代码

总结:使用pointFree的好处在于,当你有需要一个获取男性号码,或者获取所有号码的需求时不需要重写新的函数而是通过辅助函数组合。且遵守单一职责原则。

函子(Functor)

概念(什么是函子): 容器:包含值和值的变形关系(这个变形关系就是函数) 函子:是一个特殊的容器,通过一个普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行特殊处理
作用:在函数式编程中把副作用控制在可控的范围内、异常处理、异步操作等。

例子1、编写一个原始函子

    //一个容器
    class Container{
    // of 静态方法,可以省略 new 关键字创建对象
        static of(value){
            return new Container(value)
        }
        //构造函数
        constructor(value){
         this._value = value
        }
        //map 方法,传入变形关系,将容器里的每一个值映射到另一个容器
        map(fn){
            return Container.of(fn(this._value))
        }
    }
    //测试
    let functor = Container.of(3).map(x => x + 1).map(x => x * x)
    console.log(functor) //Container { _value: 36 } 结果是一个函子
复制代码

总结:

  1. 数式编程的运算不直接操作值,而是由函子完成
  2. 函子就是一个实现了 map 契约的对象

3 我们可以把函子想象成一个盒子,这个盒子里封装了一个值 4. 想要处理盒子中的值,我们需要给盒子的 map 方法传递一个处理值的函数(纯函数),由这 5. 个函数来对值进行处理 6. 最终 map 方法返回一个包含新值的盒子(函子)

MayBe函子 - 外部的空值情况做处理(控制副作用在允许的范围)

class MayBe {
    static of (value) {
        return new Container(value)
    }
    constructor (value) {
        this._value = value
    }
    map(fn) {
        return thhis.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
    isNothing () {
        return this._value === null || this._value === undefined
    }
}
//传入空值
MayBe.of(null).map(x => x.toUpperCase()) // 返回 MayBe { _value:null }
复制代码

备注:这种目前很难确认是哪一步产生的空值问题

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))
    }
}
//使用
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函子

  1. IO 函子中的_value是一个函数,这里把函数作为值来处理
  2. IO函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作纯
  3. 把不纯的操作交给调用者来处理
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()) //value 是一个函数
复制代码

例子1:通过task函子演示异步读取文件

//该函子是folktale库(一个标准的函数式编程库)中的
const { task } = require('folktale/concurrency/task') //版本2.3.2
function readFile(filename){
    return task(resolver =>{
        fs.readFile(filename,'utf-8',(err,data)=>{
            if(err) resolver.reject(err)
            resolver.resolve(data)
        })
    })
}
//调用 run 执行
readFile('test.json')
    .map(split('\n'))
    .map(find(x => x.includes('version')))
    .run().listen({
     onRejected:err =>{
        console.log(err)
     },
     onResolved:value =>{
        console.log(value)
     }
    })
复制代码

Pointed 函子 - 实现of静态方法的函子

备注:of 方法是为了避免使用 new 来创建对象,更深层的含义是 of 方法用来把值放到上下文Context(把值放到容器中,使用 map 来处理值)

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

复制代码

Monad(单子)

  1. Monad函子是可以变扁的Pointed函子,IO(IO(x))
  2. 一个函子如果具有 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函子
// IO(IO(x))
let cat = fp.flowRight(print, readFile)
// 调用 
let r = cat('package.json')._value()._value() //存在value嵌套
console.log(r)
// 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()
复制代码

总结

函数编程作为一种编程范式,随着React与Vue3的使用变得越来越受关注,在函数式编程中可以抛弃this,打包过程中也可以更好的利用tree shaking过滤无用代码以及方便测试和并行处理等优点,在未来有很好的应用场景。
函数式开发库:odash、underscore、ramda

分类:
前端
标签: