函数式编程

189 阅读18分钟

主要内容

  • 函数编程的价值以函数式编程的概念

  • 函数式编程的特性(纯函数、函子、柯里化、函数组合等)

  • 函数式编程的应用场景

  • 函数式编程库 Lodash

函数式编程的价值

函数式编程是非常古老的一个概念,早于第一台计算机的诞生,函数式编程的历史

函数式编程的意义:

  • 函数式编程是随着 React 的流行受到越来越多的关注
  • 如React 中的 HOC 使用 HOF 实现,HOF 就是函数式编程的一些特性(但 React 并非纯函数式的)。 React 的一些生态,如 Redux,也使用了函数式编程的思想(reducer、compose())
  • Vue 3也开始拥抱函数式编程,如 Vue3 的 Composition API;在 Vue2.x 的源码中也使用了大量的 HOF
  /**
   * Create a cached version of a pure function.
   */
  function cached (fn) {
    var cache = Object.create(null);
    return (function cachedFn (str) {
      var hit = cache[str];
      return hit || (cache[str] = fn(str))
    })
  }
  
  /**
   * Ensure a function is called only once.
   */
  function once (fn) {
    var called = false
    return function () {
      if (!called) {
        called = true
        fn.apply(this, arguments)
      }
    }
  }
  • 函数式编程可以抛弃 this

受到 OOP 语言的影响,在 JS中通过原型、原型链、模拟实现继承的机制,实现 js 中的面向对象编程。但是在这些过程中,烦人的 this 总是无处不在

  • 打包过程中可以更好的利用 tree shaking 过滤无用代码

  • 方便测试、方便并行处理

  • 有很多库可以帮助我们进行函数式开发: lodash、underscore、ramda

函数式编程概念

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

  • 面向对象编程的思维方式:把现实世界中的事物抽象成程序世界中的类和对象,通过封装、继承和多态来演示事物事件的联系

  • 面向过程编程:按照过程,逐步实现功能

  • 函数式编程的思维方式:把现实世界的事物和事物之间的联系抽象到程序世界(对运算过程进行抽象)

    • 程序的本质: 根据输入通过某种运算获得相应的输出,程序开发过程中会涉及很多有输入和输出的函数

    • x -> f(联系、映射) -> y,y=f(x)

    • 函数式编程中的函数指的不是程序中的函数(方法) ,而是数学中的函数即映射关系,例如:y = sin(x) ,x和y的关系

    • 相同的输入始终要得到相同的输出(纯函数)

    • 函数式编程用来描述数据(函数)之间的映射

// 非函数式(面向过程)
const num1 = 1
const num2 = 2
const sum = num1 + num2
console.log(sum)

// 非函数式(面向对象)
class Num {
  constructor(value) {
    this.value = value
  }
  add(num) {
    return this.value + num.value
  }
}
const num1 = new Num(1)
const num2 = new Num(2)
const sum = num1.add(num2)
console.log(sum)

// 函数式
// 对运算过程进行抽象,抽象为 add 函数。
// 并且相同的输入,会得到相同的输出。
// 可以在其他地方对已抽象的细粒度函数进行复用
function add (n1, n2) {
  return n1 + n2
}
const sum = add(2, 3)
console.log(sum)

****JavaScript 中关于FP的相关概念

First-class Function(头等函数)

函数是一等公民(First-class)

  • 函数可以存储在变量中

  • 函数可以作为参数和返回值(存储在变量中的体现)

在 JavaScript 中 函数就是一个普通的对象 ( new Function() ),可以把函数存储到变量中,可以作为另一个函数的参数和返回值,甚至可以在程序运行的时候通过 new Function('alert(1)') 来构造一个新的函数。

把函数赋值给变量

// 把函数赋值给变量
const fn = function () {
  console.log('Hello First-class Function')
}
fn()

const BlogController = {
  index (posts) { return Views.index(posts) },
  show (post) { return Views.show(post) },
  create (attrs) { return Db.create(attrs) },
  update (post, attrs) { return Db.update(post, attrs) },
  destroy (post) { return Db.destroy(post) }
}
const BlogController = {
  index: Views.index,
  show: Views.show,
  create: Db.create,
  update: Db.update,
  destroy: Db.destroy
}

函数是一等公民是高阶函数、函数柯里化等的前提。

函子

  • 函子是一个普通的对象,这个对象里面维护一个值,每次处理返回的是的函子对象,可以理解为对操作和值的映射关系
  • 函子是一个持有值的容器
  • 函子一般包含of(对应传入值的映射,of不是必要的),map(对应传入操作【纯函数】的映射)
    class Container {
        static of(value) {
            return new Container(value)
        }
        
        constructor(value) {
            this._value = value
        }
        
        map(fn) {
            return Container.of(fn(this._value))
        }
    }
    // 使用示例
    const r = Container.of(5)
                .map(v => v + 2)
                .map(v => v * v)
    console.log(r) // => Container { _value: 49 }

Maybe函子

在函子内部(map操作)中增加了对值的判断

    class Maybe {
        static of(value) {
            return new Maybe(value)
        }
    
        constructor(value) {
            this._value = value
        }
        
        map(fn) {
            // 使用传递的函数处理内部值时判断下
            return this.valid() ? Maybe.of(fn(this._value)) : Maybe.of(null)
        }
        
        // 创建辅助函数来判断空值
        valid() {
            return this._value != null || this._value != undefined
        }
    }
    
    // 测试:不是空值时
    const r = Maybe.of('Hello World')
        .map(x => x.toUpperCase())
        console.log(r) // => Maybe { _value: 'HELLO WORLD' }
    
    // 测试:是空值时
    const r = Maybe.of(null)
        .map(x => x.toUpperCase())
        console.log(r) // => 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))
        }
    }
    
    //创建两个函子看一下不同之处
    const l = Left.of(12).map(v => v + 2)
    const r = Right.of(12).map(v => v + 2)
    console.log(l) // => Left { _value: 12 }
    console.log(r) // => Right { _value: 14 }
    
    // 分析:两处结果不同的原因
    // Right函子中map我们是做了正常函子做的事情,得到的结果是预期的
    // Left函子中map我们是直接将当前对象返回,并没有做任何处理,其内部值不会改变
    
    // Left函子有什么作用?
    // 对于纯函数来说,相同的输入要有相同的输出,当发生异常时函子也应该给出相同的输出
    // 因此我们可以使用Left函子来处理异常
    
    // 使用示例:将字符串转成json,在转换时可能发生异常
    function parseJSON(str) {
        try {
            // 当没有异常时正常处理
            return Right.of(JSON.parse(str))
        } catch(e) {
            // 当出现异常时,我们使用Left函子来保存异常
            return Left.of({ error: e.message })
        }
    }
    // 使用
    // 出现异常的
    const errorP = parseJSON('{ name: rh }')
    console.log(errorP) // => Left { _value: { error: 'Unexpected token n in JSON at position 2' } }
    const p = parseJSON('{ "name": "rh" }')
    console.log(p) // => Right { _value: { name: 'rh' } }
    // 通过输出我们可以看到当出现异常时我们能通过Left函子来处理并存储异常
    // 当没有异常时Right函子可以正常执行

IO函子

of映射的是一个返回值的函数,map映射的是一个处理值的函数,不纯的操作交由函子外部处理

    const fp = require('lodash/fp')
    
    class IO {
        // of函数传入的还是一个值
        static of(value) {
            // 此时我们使用IO函子的构造函数
            return new IO(function() {
                // 此时我们通过函数将传递进来的值返回
                return value
            })
        }
        // 此时构造函数里边传入的是一个函数
        constructor(fn) {
            this._value = fn
        }
        
        map(fn) {
            // 返回IO,但是此时我们使用的是IO的构造函数
            // 此时我们使用fp模块中的flowRight将IO函子中存储的value(函数)和map传入的fn进行组合
            return new IO(fp.flowRight(fn, this._value))
        }
    }
    
    // 使用
    // 当前我们使用的是node环境,我们将node中的对象传递进来
    // 当调用IO的of函数时of函数会将我们传递进来值保存到一个函数中,在使用时再来获取process
    // 然后使用map来获取属性
    const io = IO.of(process).map(v => v.execPath)
    console.log(io) // => IO { _value: [Function] }
    // 通过log我们可以看到我们得到了一个io函子,函子中保存的是一个函数
    // value中的function是谁呢?我们来看一下合成过程
    // 1. of方法返回的是io对象,这个io对象中的value存储了一个函数,这个函数返回当前传入的process
    // 2. map方法返回了一个新的io函子,这个新的io函子中value保存的是经过组合的函数
    // 3. map方法中组合了fn和this._value,fn是我们传入的v => v.execPath,this._value是我们使用of得到创建的IO对象中保存的函数(即返回value那个)
    // 4. 那么我们log中得到的function就是分析3中那俩函数的组合
    
    // 获取io对象中的函数
    const ioFn = io._value
    console.log(ioFn()) // => /usr/local/Cellar/node/12.6.0/bin/node (node的执行路径)

Pointed函子

具有of的函子

Monad函子

内部有join()解决嵌套的Pointed函子,join返回的是当前value

    const fp = require('lodash/fp')
    const fs = require('fs')
    class IO {
        static of(value) {
            return new IO(function() {
                return value
            })
        }
        constructor(fn) {
            this._value = fn
        }
        
        // 我们需要在普通的IO函子中添加join方法
        join() {
            // 此处直接将当前函子的值(_value)返回(返回就是函子)
            return this._value()
        }
        
        map(fn) {
            return new IO(fp.flowRight(fn, this._value))
        }
        
        // 我们在使用Monad的时候需要将join和map结合使用
        // 此时我们再添加一个flatMap函数
        // flatMap函数的作用就是调用join和map函数将函子变扁
        flatMap(fn) { // fn供map使用
            return this.map(fn).join()
        }
    }
    
    // 使用IO函子读取文件(package.json)
    const readFile = function(fileName) {
        return new IO(function() {
            // 此处我们使用同步读取
            return fs.readFileSync(fileName, 'utf-8')
        })
    }
    
    const printV = function(v) {
        return new IO(function() {
            console.log(v)
            return v
        })
    }
    
    const r = readFile('package.json')
                .flatMap(printV)
                .join()
    console.log(r) // => package.json内容
    
    const upperR = readFile('package.json')
                    .map(fp.toUpper)
                    .flatMap(printV)
                    .join() //返回函子存储的值value
    console.log(upperR) // => 大写的package.json内容

Task函子

处理异步任务的IO函子,内含chain(用于连接两个task函子),fork(task函子内部执行),map(task函子内部处理)

  • 异步任务的实现过于复杂,我们使用folktale中的Task来演示

  • folktale一个标准的函数式编程库

    • 和lodash ramda不同的是,他没有提供很多功能函数
    • 只提供了一些函数式处理的操作,例如:compose,curry等,一些函子Task,Either,MayBe等
// Task 处理异步任务
const fs = require("fs");
const { task } = require("folktale/concurrency/task");
const { split, find } = require("lodash/fp");

function readFile(filername) {
  return task((resolver) => {
    fs.readFile(filername, "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: function (err) {
      console.log(err);
    },
    onResolved: function (data) {
      console.log(data);
    },
  });

高阶函数(Higher-order function)

高阶函数的概念

  • 函数的参数为一个函数
  // forEach
  Array.prototype.customizeForEach = function(cb) {
    for (let i = 0; i < this.length; i++) cb(this[i], i, this)
  }
  
  // filter
  Array.prototype.customiseFilter = function(cb) {
    const result = []
    for (let i = 0; i < this.length; i++) cb(this[i], i, this) && result.push(this[i])
    return result
  }
  • 函数的返回值为一个函数
  function makeFn () {
    const msg = 'Hello function'
    return function () {
      console.log(msg)
    }
  }
  const fn = makeFn()
  fn()
  
  
  // once
  function once (fn) {
    let done = false
    return function (...args) {
      if (!done) {
        done = true
        fn.apply(this, args)
      }
    }
  }
  
  const pay = once(function (money) { 
    console.log(支付: {money} RMB)
  })
  // 只会支付一次
  pay(5) // 支付 5 RMB
  pay(5)
  pay(5)

高阶函数的意义

  • 抽象可以屏蔽细节,只需要关注与我们的主要目标

  • 抽象通用的问题

// 面向过程的方式
const array = [1, 2, 3, 4]
for (let i = 0; i < array.length; i++) { // 需定义循环变量、判断循环边界、控制循环变量自增
  console.log(array[i])
}

// 高阶高阶函数
const array = [1, 2, 3, 4]
array.customizeForEach(console.log) // 不需要考虑内部如何实现,减轻开发心智负担

const result = array.customiseFilter(item => tem % 2 === 0)

常用高阶函数

  • forEach

  • map

  • filter

  • every

  • some

  • find/findIndex

  • reduce

  • sort

  • ......

// map
Array.prototype.customizeMap = function(cb) {
  const result = []
  for (let i = 0; i < this.length; i++) result.push(cb(this[i], i, this))
  return result
}

...

闭包

函数和其周围的状态(词法环境)的引用捆绑在一起形成闭包

体现

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

function makeFn () {
  const msg = 'Hello function' // makeFn 执行完后,msg 并不会被释放,msg 的作用范围得到延长
  return function () {
    console.log(msg)
  }
}
const fn = makeFn()
fn()


// once
function once (fn) {
  let done = false // // makeFn 执行完后,done 并不会被释放
  return function(...args) {
    if(done) return
    done = true
    fn.apply(this, args)
  }
}

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

场景

// 生成计算数字的多少次幂的函数
 function makePower (power) {
   return function (x) {
     return Math.pow(x, power)
   }
 }
 
 const power2 = makePower(2)
 const power3 = makePower(3)
 console.log(power2(4)) // 16
 console.log(power3(4)) // 64
 
 
 
 // 第一个数是基本工资,第二个数是绩效工资
 function makeSalary (x) {
   return function (y) {
     return x + y
   }
 }
 const salaryLevel1 = makeSalary(1500)
 const salaryLevel2 = makeSalary(2500)
 console.log(salaryLevel1(2000))
 console.log(salaryLevel1(3000))

纯函数

概念

相同的输入永远会得到相同的输出,而且没有任何可观察的副作用

纯函数就类似数学中的函数(用来描述输入和输出之间的关系),y = f(x)

  • Lodash是一个纯函数的功能库,提供了对数组、数字、对象、字符串、函数等操作的一些方法

  • 数组的 slice 和 splice 分别是:纯函数和不纯的函数

    • slice 返回数组中的指定部分,不会改变原数组
    • splice 对数组进行操作返回数组中指定部分,会改变原数组
 const numbers = [1, 2, 3, 4, 5] 
  // 纯函数
  numbers.slice(0, 3) // => [1, 2, 3] 
  numbers.slice(0, 3) // => [1, 2, 3]
  numbers.slice(0, 3) // => [1, 2, 3]
  
  // 不纯的函数
  numbers.splice(0, 3) // => [1, 2, 3]
  numbers.splice(0, 3) // => [4, 5] 
  numbers.splice(0, 3) // => []

纯函数的好处

  • 可缓存
  • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来
  const _ = require('lodash')
  function getArea (r) {
    console.log('圆的半径是 ' + r)
    return Math.PI * r * r
  }
  let getAreaWithMemory = _.memoize(getArea)
  console.log(getAreaWithMemory(4))
  console.log(getAreaWithMemory(4))
  console.log(getAreaWithMemory(4))
  
  function memoize(fn) {
    const cache = {}
    return function (...args) {
      const key = JSON.stringify(args)
      cache[key] = cache[key] || fn.apply(this, args)
      return cache[key]
    }
  }
  • 可测试
  • 纯函数让测试更方便
  • 始终有输入和输出,因此所有的纯函数都可以进行单元测试
  • 并行处理
  • 在多线程环境下并行操作共享的内存数据很可能会出现意外情况

  • 纯函数不需要访问共享的内存数据(只依赖参数的输入,不更改参数),所以在并行环境下可以任意运行纯函数 (Web Worker)

副作用

  • 纯函数:对于相同的输入永远会得到相同的输出,而且没有任何可观察的副作用
  // 不纯的
  let min = 18
  function checkAge (age) { // checkAge 的返回结果,不仅由参数 age 决定,还受到全局变量 min 影响
    return age >= min
  }
  
  // 纯的(有硬编码,后续可以通过柯里化解决)
  function checkAge (age) {
    const min = 18
    return age >= min
  }
  • 副作用让一个函数变的不纯(如上例),纯函数的根据相同的输入返回相同的输出,如果函数依赖于外部的状态就无法保证输出相同,就会带来副作用。

  • 副作用来源:

    • 配置文件
    • 数据库
    • 获取用户的输入

......

所有的外部交互都有可能带来副作用,副作用也使得方法通用性下降不适合扩展和可重用性,同时副作用会给程序中带来安全隐患给程序带来不确定性(如获取用户输入时,可能带来XSS)。

但是副作用不可能完全禁止,尽可能控制它们在可控范围内发生。

柯里化 (Haskell Brooks Curry)

  • 使用柯里化解决上一个案例中硬编码的问题
function checkAge (age) {
    let min = 18
    return age >= min
  }
  // 普通纯函数
  function checkAge (min, age) {
    return age >= min
  }
  
  checkAge(18, 24)
  checkAge(18, 20)
  checkAge(20, 30)
  
  
  // 柯里化
  function checkAge (min) {
    return function (age) {
      return age >= min
    }
  }
  // ES6 写法
  let checkAge = min => (age => age >= min)
  let checkAge18 = checkAge(18)
  let checkAge20 = checkAge(20)
  checkAge18(24)
  checkAge18(20)

柯里化的概念

  • 当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变)

  • 然后返回一个新的函数接收剩余的参数,返回相应的结果

lodash 中的柯里化函数

  _.curry(func)
  • 功能: 创建一个函数,该函数接收一个或多个 func 的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果。否则继续返回该函数并等待接收剩余的参数。

  • 参数:需要柯里化的函数

  • 返回值:柯里化后的函数

const _ = require('lodash')

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

// 柯里化后的函数
let curried = _.curry(getSum) // 对函数 getSum 进行柯里化(降元处理)
// 测试
curried(1, 2, 3)
curried(1)(2)(3)
curried(1)(2, 3)
curried(1, 2)(3)
  • 案例

''.match(/\s+/g)
''.match(/\d+/g)

function match(reg, str) {
  return str.match(reg)
}

// ---------

const _ = require('lodash')
const match = _.curry(function (reg, str) {
  return str.match(reg)
})
const findSpace = match(/\s+/g)
const findNumber = match(/\d+/g)

console.log(findSpace('hello world')) // [ ' ' ]
console.log(findNumber('25$')) // [ '25' ]

const filter = _.curry(function (func, array) {
  return array.filter(func)
})
console.log(filter(findSpace, ['John Connor', 'John_Donne']))

const findSpace = filter(findSpace)
console.log(findSpace(['John Connor', 'John_Donne']))

实现最大程度的进行函数复用

  • 模拟 _.curry() 的实现
  function curry (func) {
    return function curriedFn (...args) {
      // 判断实参和形参的个数
      if (args.length < func.length) {
        return function () {
          return curriedFn(...args.concat(Array.from(arguments)))
        }
      }
      // 实参和形参个数相同,调用 func,返回结果
      return func(...args)
    }
  }

偏函数

偏函数是 柯里化 运算的一种特定应用场景。简单描述,就是把一个函数的某些参数先固化,也就是设置默认值,返回一个新的函数,在新函数中继续接收剩余参数,这样调用这个新函数会更简单。

总结

  • 柯里化可以让我们给一个函数传递较少的参数得到一个已经记住了某些固定参数的新函数

  • 这是一种对函数参数的'缓存'

  • 让函数变的更灵活,让函数的粒度更小

  • 可以把多元函数转换成一元函数,可以组合使用函数产生强大的功能

函数组合(compose)

  • 纯函数和柯里化很容易写出洋葱代码 h(g(f(x)))

  • 获取数组的最后一个元素再转换成大写字母,如: _.toUpper(_.first(_.reverse(array)))

  • 函数组合可以把细粒度的函数重新组合生成一个新的函数

管道

下面这张图表示程序中使用函数处理数据的过程,给 fn 函数输入参数 a,返回结果 b。可以 a 数据 通过一个管道得到了 b 数据。

当 fn 函数比较复杂的时候,可以把函数 fn 拆分成多个小函数,此时多了中间运算过程产生的 m 和 n。

下面这张图中可以想象成把 fn 这个管道拆分成了3个管道 f1, f2, f3,数据 a 通过管道 f3 得到结果 m,m 再通过管道 f2 得到结果 n,n 通过管道 f1 得到最终结果 b

fn = compose(f1,f2,f3)
b = fn(a)

函数组合

概念

  • 函数组合 (compose):如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间 过程的函数合并成一个函数

    • 函数就像是数据的管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果

    • 函数组合默认是从右到左执行

// 组合函数 (仅处理两个函数)
function compose (f, g) {
  return function (x) {
    return f(g(x))
  }
}

function first (arr) {
  return arr[0]
}

function reverse (arr) {
  return arr.reverse()
}
// 从右到左运行
let last = compose(first, reverse)
console.log(last([1, 2, 3, 4]))

Lodash 中的函数组合

  • lodash 中组合函数 flow() 或者 flowRight(),都可以组合多个函数

    • flow() 是从左到右运行

    • flowRight() 是从右到左运行,使用的更多一些

const _ = require('lodash')

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

const f = _.flowRight(toUpper, first, reverse)
console.log(f(['one', 'two', 'three']))

模拟 lodash 的 flowRight 方法

// 多函数组合
function compose (...fns) {
  return function (value) {
    return fns.reverse().reduce(function (acc, fn) {
      return fn(acc)
    }, value)
  }
}

// ES6
const compose = (...fns) => value => fns.reverse().reduce((acc, fn) => fn(acc), value)

函数组合的其他概念

  • 函数的组合要满足结合律(associativity):

    • 既可以把 g 和 h 组合,还可以把 f 和 g 组合,结果都是一样的

// 结合律(associativity)
const f = compose(f, g, h)
const associative = compose(compose(f, g), h) == compose(f, compose(g, h)) // true

所以代码还可以像下面这样

const _ = require('lodash')

const f = _.flowRight(_.toUpper, _.first, _.reverse)
const f = _.flowRight(_.flowRight(_.toUpper, _.first), _.reverse)
const f = _.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))

console.log(f(['one', 'two', 'three'])) // => THREE

调试

  • 如何调试组合函数

const f = _.flowRight(_.toUpper, _.first, _.reverse)
console.log(f(['one','two','three']))

const _ = require('lodash')

const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))
const map = _.curry((fn, array) => _.map(array, fn))

const f = _.flowRight(
  join('-'),
  trace('map 之后'),
  map(_.toLower),
  trace('split 之后'),
  split(' ')
)
console.log(f('NEVER SAY DIE'))

lodash/fp

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

  • 提供了不可变 auto-curried iteratee-first data-last 的方法

// lodash 模块
const _ = require('lodash')
_.map(['a', 'b', 'c'], _.toUpper) // => ['A', 'B', 'C']
_.map(['a', 'b', 'c']) // => ['a', 'b', 'c']

_.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')
const fp = require('lodash/fp')

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

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

Point Free

  • 可以把数据处理的过程定义成与数据无关的合成运算,不需要用到代表数据的那个参数,只要把简单的运算步骤合成到一起,在使用这种模式之前我们需要定义一些辅助的基本运算函数。
  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
  const f = fp.flowRight(fp.join('-'),fp.map(_.toLower),fp.split(''))
  • 案例
  // 非 Point Free 模式
  // Hello World => hello_world
  function f (word) {
    return word.toLowerCase().replace(/\s+/g, '_')
  }
  
  // Point Free
  const fp = require('lodash/fp')
  
  const f = fp.flowRight(fp.replace(/\s+/g, '_'), fp.toLower)
  console.log(f('Hello World'))
  • 使用 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)

「前端进阶」彻底弄懂函数柯里化

函数式夜点心:异步流程与 Task 函子