1. 函数式编程

1.1. 函数是一等公民

函数式编程(Functional Programming, FP)

  • 面向对象编程: 把现实世界中的事物抽象成程序世界中的类和对象,通过封装,集成,多态来演示事物事件的联系
  • 函数式编程: 把现实世界的事物和事物之间的联系抽象到程序世界,对运算过程进行抽象
  • 思维方式 函数式编程中的函数不是指程序中的函数,更多的是数学中的函数即映射关系 相同的输入始终得到相同的输出(纯函数) 函数式编程用来描述函数之间的映射
  • 好处: 让函数,逻辑等得到最大程度的重用
  • 坏处: 大量使用闭包,某种程度上会降低性能

一等公民体现在函数可以作为变量,可以作为参数,可以作为返回值

  • 作为变量时,应用场景如mvc中签名相同的函数

使用高阶函数意义

  • 屏蔽细节,只需关注我们的目标
  • 高阶函数是用来抽象通用的问题

1.2. 纯函数

如数组的slice和splice方法分别为纯函数和非纯函数

相同的输入得到相同的输出,没有副作用

纯函数好处

  • 可以把纯函数的结果缓存起来,节省计算性能
    function memoize(fn) {
    let cache = {}
    return function() {
      let key = JSON.stringify(arguments)
      cache[key] = cache[key] || fn.apply(f, arguments)
      return cache[key]
    }
    }
    
  • 让测试更方便
  • 并行环境下可以方便并行处理(web worker)

副作用

副作用来源,依赖外部状态

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

副作用不可避免,只能最大程度控制在可控范围内

1.3. 柯里化

函数有多个参数时,调用一个函数传递部分参数,返回一个新的函数,新函数等待接收剩余参数,返回最终结果

tips: 注意传参的顺序性

let checkAge = min => (age => age >= min)
let checkAge18 = checkAge(18)
console.log(checkAge18(20))

1.3.1. lodash中的柯里化函数

_.curry(func)

作用: 将多元函数变成单元函数,方便组合使用

function getSum (a, b, c) {
  return a + b + c
}
const curried = _.curry(getSum)
console.log(curried(1, 2, 3), curried(1)(2, 3))

// 去除数组空白项
const match = _.curry(function (reg, str) {
  return str.match(reg)
})
const haveSpace = match(/\s+/g)
let filter = _.curry((func/* 筛选数组项的函数 */, arr) => arr.filter(func))
const findSpace = filter(haveSpace)

实现原理

function curry (func) {
  return function curriedFn (...args) {
    // 判断实参args和形参个数
    if(args.length < func.length) {
      return function () {
        return curriedFn(...args.concat(Array.from(arguments)))
      }
    }
    return func(...args)
  }
}

总结

  • 使用闭包缓存函数参数
  • 让函数变得更灵活,粒度更小
  • 可以把多元函数转换成一元函数,组合函数产生强大的功能

1.4. 函数组合

纯函数和柯里化容易写出洋葱代码,通过函数组合可以把细粒度的函数重新组合生成一个新的函数

关于函数组合(compose)

  • 函数就像是数据的管道,函数组合就是把管道连接起来,让数据穿过多个管道
  • 函数组合默认从右到左执行

1.4.1. lodash中的函数组合

flow,flowRight

const _ = require('lodash')

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

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

实现原理

// function compose (...args) {
//   return function (value) {
//     return args.reverse().reduce(function(acc, fn) {
//        return fn(acc)
//     }, value)
//   }
// }

const compose = ...args => value => args.reverse().reduce((acc, fn) => fn(acc), value)

函数组合要满足结合律

// 如得到一个数组最后一项的大写
_.flowRight(_.toUpper, _.flowRight(_.first, _.reverse))

1.4.2. compose调试

在函数前后加上log函数,如

// 缺点无法准确定位数据来自哪个管道
const log = v => {
  console.log(v);
  return v
}

// 添加标记,使用柯里化后的log函数标记
const trace = _.curry((tag, v) => {
  console.log(tag, v)
  return v
})

在使用函数组合的适合,如果使用lodash中的函数,如果函数有多个函数,需要重新包装函数对其柯里化,比较麻烦

1.5. lodash中的fp模块

lodash/fp模块提供了对函数式编程友好的方法,及不可变的方法(auto curried iteratee-first data-last)

const _ = require('lodash')
const fp = require('lodash/fp')

_.map(['a', 'b', 'c'], _.toUpper)

fp.map(fp.toUpper, ['a', 'b', 'c'])  // 或
fp.map(fp.toUpper)(['a', 'b', 'c'])

案例

// NEVER SAY DIE --> never-say-die
const fp = require('lodash/fp')

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

lodash中map方法的问题

_.map第二个参数的函数需要接收三个参数, value, index|key, collection,当调用parseInt时,会有预料之外的结果,如

_.map(['23', '8', '10'], parseInt)    // 结果为[23, NaN, 2],原因是parseInt第二参数为进制

fp模块中的map方法中的函数参数只有一个参数, 无此问题

1.6. 函子(Functor)

  • 容器: 包含值和值的变形关系
  • 函子: 是一个特殊容器,通过一个普通对象实现,该对象具有map方法,map方法可以运行一个函数对值进行处理.

作用: 控制副作用,处理异常,进行异步操作

class Container {
  constructor (value) {
    // 函子里要维护一个值,这个值不对外公布.想要处理值,通过map方法,传递处理值的函数
    this._value = value
  }
  map (fn) {
    return new Container(fn(this._value))
  }
}

new Container(5)
  .map(x => x + 1)
  .map(x => x * x)

以上写法为面向对象的思想,改造为函数式编程的思想如下

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

总结

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

1.6.1. MayBe函子

编程过程中出现的错误,需要做出相应处理.MayBe函子可以对外部的空值情况做处理(控制副作用在允许的范围)

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

let r = MayBe.of('hello world')
          .map(x => x.toUpperCase())
          .map(x => null)

1.6.2. Either函子

  • 能给出有效提示信息
  • 类似于if...else...的处理
  • 异常会让函数变得不纯,Either函子可以用来做异常处理
class Left {
  constructor (value) {
    this._value = value
  }

  static of (value) {
    return new Left(value)
  }

  map (fn) {
    return this
  }
}

class Right {
  constructor (value) {
    this._value = value
  }

  static of (value) {
    return new Right(value)
  }

  map (fn) {
    return Right.of(fn(this._value))
  }
}

let r1 = Right.of(12).map(x => x + 2)   // 14
let r2 = Left.of(12).map(x => x + 2)    // 12

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

1.6.3. IO函子

  • IO函子中的_value是一个函数,即把函数当做值来处理
  • IO函子可以把不纯的行为存储到_value中,延迟执行不纯的操作,包装的操作为纯的
  • 把不纯的操作交给调用者处理
class IO {
  constructor (fn) {
    this._value = fn
  }

  static of (value) {
    return new IO(function () {
      return value
    })
  }

   map (fn) {
     // 把第一次调用of传入的函数包装后的函数同传入的函数组合
     return new IO(fp.flowRight(fn, this._value))
   }
}

// 调用
let r = IO.of(process).map(p => p.execPath)
console.log(r._value())

1.6.4. folktale

一个标准的函数式编程库,与lodash, ramda不同的是,没有提供很多功能函数,只提供了一些函数式处理的操作如函数组合,柯里化等,一些函子

const { compose, curry } = require('folktale/core/lamda')

// curry接收两个参数,参数一为参数二函数中的参数个数

1.6.5. Task函子

进行异步处理

读取packge.json version

const fs = require('fs')
const { task } = require('folktale/concurrency/task')
const { split, find } = require('lodash/fp')

function readFile (filename) {
  return task(resolver => {   // task函数返回一个Task类型的函子
    fs.readFile(filename, 'utf-8', (err, data) => {
      if(err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}

readFile('package.json')
  .map(spit('\n'))
  .map(found(x => x.inclueds('version')))
  .run()
  .listen({
    onRejected: err => {

    },
    onResolved: value => {

    }
  })

1.6.6. Pointed函子

  • 实现了of静态方法的函子
  • 为了避免使用new来创建对象,更深的含义是of方法用来把值放到上下文,即函子中

1.6.7. Monad函子

上面的IO函子的写法,会带来IO函子嵌套的问题,因此Monad可以解决此问题,它是一个实现了join和静态of方法的函子,用于拍平Pointed函子

实现linux/unix下cat命令效果

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

  map (fn) {
    return new Monad(fp.flowRight(fn, this._value))
  }

  join () {
    return this._value()
  }

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

let readFile = function (filename) {
  return new Monad(function () {
    return fs.readFileSync(filename, 'utf-8')
  })
}

let print = function (x) {
  return new Monad(function () {
    console.log(x)
    return x
  })
}

// 调用map还是flatMap?当我们要合并的这个函数返回的是值,调用map,返回的是函子,返回flatMap
let r = readFile('package.json')
          // 可以在这里使用map对读取后的值进行操作
          .flatMap(print)
          .join()

console.log(r)
Jason Huang all right reserved,powered by Gitbook该文件最后修改时间: 2020-11-02 15:28:47

results matching ""

    No results matching ""