概念

  • 函数式编程(Functional Programming, FP),FP 是编程范式之一。
  • 对运算过程进行抽象,描述数据(函数)之间的映射关系。
  • 相同的输入始终得到相同的输出(纯函数)。

函数是一等公民

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

    高阶函数

  • 可以把函数作为参数传递给另一个函数

  • 可以把函数作为另一个函数的返回结果

    使用改节函数的意义

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

  • 高阶函数是用来抽象通用的问题

    1. function once(fn) {
    2. let done = false;
    3. return function () {
    4. if (!done) {
    5. done = true;
    6. return fn.apply(this, arguments);
    7. };
    8. }
    9. }

    闭包(Closure)

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

  • 可以在另一个作用域中调用一个函数的内部函数并访问到该函数的作用域中的成员
  • 与V8引擎的垃圾回收机制有关,内部函数作用域链的第二层存着外部函数的执行期上下文

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

纯函数

相同的输入永远会得到相同的输出, 而且没有任何可观察的副作用,每次调用返回值都相同的是纯函数

  • 数组中的slice 和splice分别是纯函数和不纯的函数
    • slice 返回数组中的指定部分,不会改变原数组
    • splice对数组进行操作返回该数组,会改变原数组
  • 函数式编程不会保留计算中间的结果,所以变量是不可变的
    • 因此需要许多细粒度的纯函数来帮助处理,需要使用到函数式编程库例如:lodash
  • 我们可以把一个函数的执行结果交由另一个函数去处理
  • 好处:
    1. 可缓存
      • 因为纯函数对相同的输入始终有相同的结果,所以可以把纯函数的结果缓存起来 ``` // lodash中的memoize 记忆函数

function getArea(r) { return Math.PI r r }

let getAreaWithMemery = lodash.memoize(getArea) console.log(getAreaWithMemery(4))

// 模拟记忆函数 funtion memoize(f) { let cache = {} return function () { let key = JSON.stringify(arguments) cache[key] = cache[key] || f.apply(f, arguments) return cache[key] } }

  1. 1. 可测试
  2. - 纯函数让测试更方便
  3. 2. 并行处理
  4. - 在多线程环境下并行操作共享的内存数据很可能会出现意外情况
  5. - 纯函数不需要访问共享的内存数据,所以在并行环境下可以任意运行纯函数(Web Worker)
  6. **副作用**让一个函数变得不纯,如果函数依赖于外部的状态就无法保证输出相同,就会带来负**副作用**
  7. - 来源
  8. 1. 配置文件
  9. 1. 数据库
  10. 1. 获取用户输入
  11. <a name="Dp0Ax"></a>
  12. ## 柯里化
  13. 当函数有多个参数,可以利用闭包将函数的部分参数缓存,返回新函数接收剩余参数,返回结果。
  14. ```javascript
  15. function curry (prev) {
  16. return function (next) {
  17. return prev + next;
  18. }
  19. }

lodash中通用的柯里化

_.curry(func)

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

  1. function curry(fn) {
  2. return function curriedFn(...args) {
  3. if (args.length < fn.length) {
  4. return function () {
  5. return curriedFn(...args.concat(Array.from(arguments)))
  6. }
  7. }
  8. return fn(...args)
  9. }
  10. }

总结

  • 柯里化可以让我们传递少于形参的参数得到一个记住已经传递的参数的新函数。
  • 这是一种对函数参数的缓存
  • 让函数变得更灵活,颗粒度更小。
  • 可以把多元函数变成一元函数,可以组合使用函数产生强大的功能。

函数组合

  • 将细粒度的函数重新组合生成一个新的函数
  • 纯函数与柯里化很容易写出洋葱代码h(g(f(x))),例如获取数组最后一个元素再转换成大写字母,.toUpper(.first(_.reverse(array)))

    概念

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

  • 函数就像是数据管道,函数组合就是把这些管道连接起来,让数据穿过多个管道形成最终结果
  • 函数组合默认是从右到左执行

lodash中的组合函数

  • flow() 或者 flowRight(), 他们都可以组合多个函数
  • flow() 是从左到右运行
  • flowRight() 是从右到左运行,使用的更多一些
  • 函数的组合要满足结合律,我们既可以把g和h组合,还可以把f和g组合,结果都是一样的 ```javascript function compose (…args) { return function (value) {
    1. return args.reverse().reduce((prev, acc) => acc(prev), value);
    } }

const compose = (…args) => (value) => args.reverse().reduce((prev, acc) => acc(prev), value);

  1. <a name="5gyrN"></a>
  2. ### lodash-fp
  3. _lodash的fp模块提供了实用的对函数式编程友好的方法,_**_函数优先,数据滞后_**_,都是被_**_柯里化_**_的函数,可以直接用来组合。_
  4. ```javascript
  5. const fp = require('lodash/fp');
  6. const _last = fp.flowRight(fp.toUpper, fp.last, fp.map(value => fp.prop('name')))

PointFree

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
  • 其实就是函数组合

函子Functor

一个特殊的容器,通过普通的对象来实现,该对象具有map方法,map方法可以运行一个函数对值进行处理(变形关系)。

  1. class Container {
  2. static of(value) {
  3. return new Container(value);
  4. }
  5. constructor(value) {
  6. this._value = value;
  7. }
  8. map(fn) {
  9. return Container.of(fn(this._value));
  10. }
  11. isNothing(
  12. }
  • 总结

    • 函数式编程的运算不直接操作值, 而是由函子完成
    • 函子就是一个实现了map契约的对象
    • 想要处理函子中的值,需要给函子中的map方法传递一个处理值的函数(纯函数),由这个函数来对值进行处理
    • 最终map方法返回一个包含新值的函子

      Maybe函子

  • Maybe函子的作用就是可以对外部空值做处理

    Either函子

  • Either两者中的任何一个,类似于if else 的处理

  • 异常会让函数变得不纯,Either函子可以用来做异常处理

    IO函子

  • IO 函子中的_value 是一个函数,这里是把函数作为值来处理

  • IO 函子可以把不纯的动作存储到_value中,延迟执行这个不纯的操作(惰性执行),包装当前的操作
  • 把不纯的操作交给调用者来处理 ```javascript const fp = require(“lodash/fp”) class IO { static of (x) {
    1. return new IO(function () {
    2. return x
    3. })
    } constructor (fn) {
    1. this._value = fn
    } map (fn) {
    1. // 把当前的value和传入的fn组合成一个新函数
    2. return new IO(fp.flowRight(fn, this._value))
    } }

let r = IO.of(process).map(p => p.execPath) // 将不纯的操作延迟到调用的时候处理 console.log(r._value())

  1. - 缺点:调用嵌套函子的时候不方便
  2. <a name="9slUq"></a>
  3. ### folktale
  4. - folktale是一个标准的函数式编程库
  5. - lodashramda不同的是,他没有提供很多功能函数
  6. - 只提供了一些函数式处理的操作、例如:composecurry等,一些函子TaskEitherMayBe
  7. ```javascript
  8. const { compose, curry } = require("folktale/core/lambda")
  9. const { toUpper, first } = require("lodash/fp")
  10. let f = curry(2, (x, y) => x + y)
  11. let n = compose(toUpper, first)

Task 函子

异步执行,处理异步任务

  1. const fs = require("fs")
  2. const { task } = require('folktale/concurrency/task')
  3. const { split, find } = require('lodash/fp')
  4. function readFile (filename) {
  5. return task(resolver => {
  6. fs.readFile(filename, "utf-8", (err, data) => {
  7. if(err) resolver.reject(err)
  8. resolver.resolve(data)
  9. })
  10. })
  11. }
  12. readFile('package.json')
  13. .map(split('\n'))
  14. .map(find(x=> x.includes('version')))
  15. .run()
  16. .listen({
  17. onRejected: err => {
  18. console.log(err)
  19. },
  20. onResolved: value => {
  21. console.log(value)
  22. }
  23. })

Pointed 函子

  • 是实现了of静态方法的函子
  • of方法是为了避免使用new来创建对象,更深层的含义是of方法用来把值放到上下文 context(把值放到容器中,使用map来处理值)

Monad 函子

  • Monad函子是可以变扁的Pointed 函子
  • 一个函子如果具有joinof两个方法并遵守一些定律就是一个Monad
  1. const fp = require('lodash/fp')
  2. class IO {
  3. static of (x) {
  4. return new IO(function () {
  5. return x
  6. })
  7. }
  8. constructor(fn) {
  9. this._value = fn
  10. }
  11. map (fn) {
  12. return new IO(fp.flowRight(fn, this._value))
  13. }
  14. join () {
  15. return this._value()
  16. }
  17. flatMap(fn) {
  18. return this.map(fn).join()
  19. }
  20. }

Either 两者中的任何一个,类似于 if…else…的处理异常会让函数变的不纯,Either 函子可以用来做异常处理

编程范式之一