函数式编程-学习笔记

权弘新
2023-12-01

一、认识函数式编程

函数式编程的思想:把现实世界中事物与事物的联系抽象到程序中。即把数据处理的运算过程抽象出来。

函数式编程的思想与面向对象编程的思想是并列的,面向对象编程思想是把现实世界中的事物抽象成程序世界中的类和方法。

函数式编程中的函数指的是数学概念中的映射关系。

函数式编程的好处是:抛弃this、打包过程中更好的利用tree shaking过滤无用代码、方便测试和并行处理、有丰富的库的支持如lodash ramda folktale。

二、函数相关概念

1.函数是一等公民

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

2.高阶函数

有函数作为参数或返回值的函数,就是高阶函数。

高阶函数的核心思想就是把运算过程抽象出来,使得代码更加简洁、灵活、通用。

常用的高阶函数如forEach map filter every等。

3.闭包

闭包就是在函数外部借由访问函数内部的方法,来达到访问函数内部变量的目的。

函数内部的局部变量在函数的调用执行完毕后本应销毁,但由于有了函数内部方法的暴露(该内部方法访问了函数内部变量),使得该局部变量作用范围延长。

闭包示例:

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

let pay = once(function(amount){
    console.log(`Paied ${amount} 元`)
})

pay(6)
pay(6)
pay(6) // pay仅执行一次

三、函数式编程基础

1.lodash

lodash是一个模块化的纯函数的库。

// 安装 npm i lodash --save

const _ = require('lodash')
const first = _.first([1,3,5]) // 返回数组第一个元素 1


// Load the FP build for immutable auto-curried iteratee-first data-last methods.
// loadash中的函数式编程模块functional programming
const fp = require('lodash/fp') 
fp.map(parseInt, ['23', '8', '10'])

2.纯函数


纯函数:相同的输入永远会得到相同的输出。如slice方法就是一个纯函数,它无任何副作用;而splice方法就是一个不纯的函数,会产生副作用。

3.柯里化


柯里化:当一个函数有多个参数的时候先传递一部分参数调用它,然后返回一个新的函数来接收剩余的参数,返回结果。

柯里化的好处:缓存函数的部分参数、让函数粒度更小更灵活、可以把多元函数转换成一元函数,组合使用来产生强大的功能。
柯里化示例:

// 示例一
function checkAge(min){
    return function(age){
        return age >= min
    }
}
const checkAdult = checkAge(18)
const checkCanGetMarry = checkAge(22)
checkAdult(19)        // true
checkCanGetMarry(25)  // true



// 示例二 使用lodash中的柯里化方法 curry
const _ = require('lodash')

function getSum(a, b, c){
    return a + b + c
}

const curriedFn = _.curry(getSum)
curriedFn(1,2,3)   // 6
curriedFn(1,2)(3)  // 6
curriedFn(1)(2)(3) // 6

柯里化原理:

function myCurry(fn) {
    return function innerCurried (...args){ // args第一批参数
        if(args.length < fn.length){ // 实际调用时传入参数的个数 < 将被柯里化的函数的形参的个数
            return function(...thenArgs){ // thenArgs第二批参数
                return innerCurried(...(args.concat(thenArgs)))
            }
        }
        return fn(...args)
    }
}

function getSum(a, b, c){
    return a + b + c
}

const curried = myCurry(getSum)
console.log(curried(1,2,3))  
console.log(curried(1,2)(3))
console.log(curried(1)(2,3))
console.log(curried(1)(2)(3))

4.函数组合

函数组合:把细粒度的函数重新组合成一个新的函数,避免层层嵌套的洋葱代码。
函数的组合需要满足结合律,比如fn(f, g, h),结合律就是既可以把f h组合,也可以把g h组合,结果一样。
函数组合示例:

// 示例一
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,8]))


// 示例二 lodash中的函数组合
const _ = require('lodash')

const reverse = arr => arr.reverse()
const first = arr => arr[0]
const toUpper = value => value.toUpperCase()

const composed = _.flowRight(toUpper, first, reverse) // _.flowRight 从右到左开始执行
console.log(composed(['danny', 'janney', 'lily'])) // LILY

四、函子

1.Functor

函子-Functor,函子是一个实现了map契约的对象,函子好比一个盒子,里面封装了一个值,想处理盒子里的值,我们需要给函子的map方法传递一个处理值得纯函数,由该函数对值进行处理,最终map方法返回一个包含了新值得函子(盒子)。

函子示例:

// 函子
class Container{
    constructor(value){
        this._value = value
    }
    static of(value){
        return new Container(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); return x}) // 49
console.log('r: ', r); // r:  Container { _value: 49 }

2.MayBe函子

MayBe函子用来处理异常的空值。
MayBe函子示例:

// MayBe函子
class MayBe{
    constructor(value){
        this._value = value
    }
    static of(value){
        return new MayBe(value)
    }
    isNothing(){
        return this._value === null || this._value === undefined
    }
    map(fn){
        return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
    }
}

const r = MayBe.of(null)
            .map(x => x.toUpperCase())
            .map(x => x.toLowerCase())
            .map(x => x + ' ' + x)
console.log(r) // MayBe { _value: null }

3.Either

Either函子用来处理异常分支。

Either函子示例:

// Either函子
class Left{
    constructor(value){
        this._value = value
    }
    static of(value){
        return new Left(value)
    }
    map(){
        return this
    }
}

class Right{
    constructor(value){
        this._value = value
    }
    static of(value){
        return new Right(value)
    }
    map(fn){
        return Right.of(fn(this._value))
    }
}

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

console.log(parseJSON('{ name: Lily }'))     // Left { _value: 'error: Unexpected token n in JSON at position 2' }
console.log(parseJSON('{ "name": "Lily" }')) // Right { _value: { name: 'Lily' } }

4.IO

IO函子中的_value是一个函数。
IO函子可以把不纯的动作存储到_value中,在调用的时候再延迟执行这个不纯的操作,把当前操作包装成纯的操作,把可能的不纯的操作交给调用者来处理。
IO函子示例:

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

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

  static of(value){ // value是数据
    return new IO(function(){
      return value
    })
  }

  map(fn){
    // 把当前的value 和 传入的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)   // [Function]
console.log(r._value()) // C:\Program Files\nodejs\node.exe

5.Task - folktale中的task

folktale是一个标准的函数式编程的库,与lodash、ramda不同的是:它并未提供很多功能函数,只提供了一些函数式处理的操作。
如 curry compose 和 一些函子如 MayBe Either Task等。
folktale的柯里化方法和函数组合方法示例:

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

// folktale的柯里化方法 第一个参数是传入函数的参数个数
let f = curry(2, (a, b) => console.log(a + b))
f(3, 4) // 7
f(3)(4) // 7


// folktale的函数组合方法
let f2 = compose(toUpper, first)
console.log(f2(['abc', 'def'])) // ABC

folk的task处理异步任务示例:

// Task处理异步任务
const fs = require('fs') // node环境的文件操作模块
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-lock.json') // 此时返回的是一个task函子 每个函子都有map方法
  .map(split('\n')) // map方法接收方法
  .map(find(x => x.includes('version')))
  .run() // task函子提供的执行方法
  .listen({
    onRejected: err => {
      console.log(err)
    },
    onResolved: value => {
      console.log(value) // "version": "2.3.2",
    }
  })

6.Monad

Pointed函子就是实现了 of 静态方法的函子,of方法是为了避免使用new 来创建对象,深层含义是 of方法用来把值放到上下文Context(把值放到容器中 使用map来处理值)。
Monad函子是可以变扁的Pointed函子,IO(IO(x)),一个函子如果具有join和of两个方法并遵守一些定律就是一个Monad函子。
Monad函子示例:
 

// Monad函子
const fs = require('fs')
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))
  }

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

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

let r = readFile('../package-lock.json')
          .map(x => x.toUpperCase())
          .flatMap(print)
          .join()
console.log(r) // 大写的package-lock.json文件的内容

本文 完。

 

 类似资料: