JS 函数式编程 02 —— 函数组合,Pointfree,Functor(函子)

JS 函数式编程 02

  • 函数组合
    • 管道
    • lodash 中的组合函数 flow() or flowRight()
    • 函数结合律
    • 函数组合的调试
      • lodash中的fp模块
  • Pointfree
  • Functor
    • 什么是函子,作用是什么?
    • 什么是Functor
    • 常见函子
      • Maybe 函子
      • Either函子
      • IO函子
      • Task函子(异步执行)
      • Pointed函子
      • Monad函子(单子)

函数组合

为什么要使用函数组合?
因为纯函数和柯里化会很容易就形成洋葱代码 ( 多个括号嵌套 )
比如: 获取数组中最后一个元素 并且把它转化为大写

_.toUpper(_.first(_.reverse(array)))

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

管道

程序使用函数处理数据的过程可以看做是一个管道
比如: 数据a ——> 通过函数fn ——> 得到结果b
当fn比较复杂的时候,我们可以把fn拆分成多个小函数
比如: 数据a ——> 通过f1——>得到m——> 通过f2——> 得到b
类似下面这种代码

fn = compose(f1,f2,f2)
b = fn(a)
  • 函数组合( compose ) :如果一个函数要经过多个函数处理才能得到最终值,这个时候可以把中间过程的函数合并成一个函数
  • 函数组合默认是从右到左执行
    使用代码演示:虽然看起来似乎把问题复杂化了,但是要注意 我们使用函数组合 可以自由组合细粒度的函数
// 函数组合演示
function compose(f, g) {
     
  return function (value) {
     
    return f(g(value))
  }
}

// 数组翻转函数
function reverse (array) {
     
  return array.reverse()
}

// 获取函数第一个元素函数
function first (array) {
     
  return array[0]
}

// 组合函数,获取函数最后一个元素
const last = compose(first, reverse)

console.log(last([1, 2, 3, 4])) // 4

lodash 中的组合函数 flow() or flowRight()

lodash 中组合函数 flow() 或者flowRight()

  • flow() 是从左到右运行
  • flowRight() 是从右到左运行
  • 获取数组中最后一个元素并将它转化为大写
const _ = require( "lodash" )
const reverse = arr=>arr.reverse()
const first = arr=>arr[0]
const toUpper = str => str.toUpperCase()

const fn = _.flowRight(toUpper, first, reverse)
console.log(fn(['tom','jerry','jim','lucy'])) //LUCY
  • 模拟lodash中的 flowRight方法
    1. 定义一个函数接收不固定数量的形参 compose( …args )
    2. 返回一个新的函数 return function 需要接收一个要处理的值 作为形参
    3. 让args中的成员依次执行,这里可以使用数组的reduce方法
    function compose(...args){
           
    	return function(value){
           
    	// args是要依次执行的函数数组,要从右到左执行,所以需要先reverse
    	// reduce 是数组方法,第一个参数是一个回调,回调必须填入两个形参,
    	// 形参1 表示初始值或者回调函数计算后的值
    	// 形参2 表示当前元素 
    	// 比如 [1,2,3].reduce((a,b)=> a+b ) //第一次执行 a就是1,b就是2 第二次执行 a是3 b是3
    	// reduce第二个参数 是用于指定初始值 也就是指定回调的第一个形参的初始值 
    	// 我们设置为value 这样第一次执行的时候 result就是value initFn就是args中的第一个成员函数
    		return args.reverse().reduce((result,initFn)=>{
            
    				return initFn(result)
    		 },value)
    	}
    }
    // 可以通过箭头函数简化代码
    const compose = (...args) => value => args.reverse().reduce((result,initFn)=>initFn(result),value) 
    

函数结合律

函数的组合要满足结合律
比如 a 和 b 组合 再跟 c组合 等效与 b先跟c组合 再跟a组合

(A,B),C 与 A,(B,C) 等效

函数组合的调试

在函数组合的管道中,每一个细粒度的函数只能接收一个参数即上一个函数的执行结果。所以如果某一个函数需要多个参数 那么我们需要对其做柯里化处理
我们采取定义一个打印函数用来调试

// NEVER SAY DIE --> nerver-say-die

const _ = require('lodash')
// 对需要多个参数的函数进行柯里化
const split = _.curry((sep, str) => _.split(str, sep))
const join = _.curry((sep, array) => _.join(array, sep))

// 我们需要对中间值进行打印,并且知道其位置,用柯里化输出一下
const log = _.curry((tag, v) => {
     
  console.log(tag, v)
  return v
})

// 从右往左在每个函数后面加一个log,并且传入tag的值,就可以知道每次结果输出的是什么
const f = _.flowRight(join('-'), log('after toLower:'), _.toLower, log('after split:'), split(' '))
// 从右到左
//第一个log:after split: [ 'NEVER', 'SAY', 'DIE' ] 正确
//第二个log: after toLower: never,say,die  转化成小写字母的时候,同时转成了字符串,这里出了问题
console.log(f('NEVER SAY DIE')) //n-e-v-e-r-,-s-a-y-,-d-i-e


// 修改方式,利用数组的map方法,遍历数组的每个元素让其变成小写 
// 这里的map需要两个参数,第一个是数组,第二个是回调函数,需要柯里化
const map = _.curry((fn, array) => _.map(array, fn))

const f1 = _.flowRight(join('-'), map(_.toLower), split(' '))
console.log(f1('NEVER SAY DIE')) // never-say-die

lodash中的fp模块

因为函数组合中函数只能接收一个参数,这样需要对已有的函数做大量柯里化的工作,我们可以使用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', ' ') 

//BUT
// 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')

Pointfree

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

  • 不需要指明处理的数据
  • 只需要合成运算过程
  • 需要定义一些辅助的基本运算函数
// world wild web ---> World.Wild.Web
// 先转成数组
// 再把首字母换成大写

const fp = require('lodash/fp')

const firstLetterToUpper = fp.flowRight(fp.join('.'),fp.map(fp.upperFirst),fp.split(' '))

console.log(firstLetterToUpper('world wild web'));

Functor

什么是函子,作用是什么?

函子( representative functor ) 是范畴论里的概念,我们没有办法避免副作用,但是可以通过函子让副作用控制在可控范围内,同时也可以通过函子处理异常,异步等

什么是Functor

  • 容器:包含 和 值的变形关系 ( 即 函数 函数处理值)
  • 函子是一个特殊的容器,通过对象实现,具有map方法,map方法运行一个函数对值进行处理
class Container() {
     
	constructor(value){
      
		this._value = value // 加_表示永远不暴露该属性
	}
	map(fn){
     
		// 调用value的变形关系 fn 
		// 返回一个新的函子实例 其中的_value就是上一次fn运算的结果
		return new Container(fn(value))
	}
}

因为还是有new的存在是面向对象思想 所以我们修改为函数式编程

class Container() {
     
	static of(value){
     
		return new Container(value) 
	}
	constructor(value){
      
		this._value = value // 加_表示永远不暴露该属性
	}
	map(fn){
     
		// 调用value的变形关系 fn 
		// 返回一个新的函子实例 其中的_value就是上一次fn运算的结果
		return Container.of(fn(value))
	}
}
  • 函数式编程不直接操作值,而是由函子完成
  • 函子就是一个容器 一个对象 实现了map契约
  • 函子可以看做是一个容器里面装了一个值
  • 想要处理其中的值,我们需要给盒子的map传递处理值的纯函数
  • map最终会返回一个包含新值的容器 (函子)

常见函子

Maybe 函子

可以对外部的空值情况做处理(控制副作用在允许的范围)

		class MayBe {
     
		  static of (value) {
     
		    return new MayBe(value)
		  }
		  constructor (value) {
     
		    this._value = value
		  }

		  map(fn) {
     
		    // 判断一下value的值是不是null和undefined,如果是就返回一个value为null的函子,如果不是就执行函数
		    return this.isNothing() ? MayBe.of(null) : MayBe.of(fn(this._value))
		  }
		
		 // 定义一个判断是不是null或者undefined的函数,返回true/false
		  isNothing() {
     
		    return this._value === null || this._value === undefined
		  }
		}
		
		const r = MayBe.of('hello world')
		  .map(x => x.toUpperCase())
		
		console.log(r) //MayBe { _value: 'HELLO WORLD' }
		
		
		// 如果输入的是null,是不会报错的
		const rnull = MayBe.of(null)
		  .map(x => x.toUpperCase())
		console.log(rnull) //MayBe { _value: null }

Either函子

  • Either 两者中的任何一个,类似于 if…else…的处理
  • 当出现问题的时候,Either函子会给出提示的有效信息,
  • 异常会让函数变的不纯,Either 函子可以用来做异常处理
// 因为是二选一,所以要定义left和right两个函子

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

let r1 = Right.of(12).map(x => x + 2)
let r2 = Left.of(12).map(x => x + 2)
console.log(r1) // Right { _value: 14 }
console.log(r2) // Left { _value: 12 }
// 为什么结果会不一样?因为Left返回的是当前对象,并没有使用fn函数

// 那么这里如何处理异常呢?
// 我们定义一个字符串转换成对象的函数
function parseJSON(str) {
     
  // 对于可能出错的环节使用try-catch
  // 正常情况使用Right函子
  try{
     
    return Right.of(JSON.parse(str))
  }catch (e) {
     
  // 错误之后使用Left函子,并返回错误信息
    return Left.of({
      error: e.message })
  }
}

let rE = parseJSON('{name:xm}')
console.log(rE) // Left { _value: { error: 'Unexpected token n in JSON at position 1' } }
let rR = parseJSON('{"name":"xm"}')
console.log(rR) // Right { _value: { name: 'xm' } }

console.log(rR.map(x => x.name.toUpperCase())) // Right { _value: 'XM' }

IO函子

  • IO就是输入输出,IO 函子中的 _value 是一个函数,这里是把函数作为值来处理
  • IO 函子可以把不纯的动作存储到 _value 中,延迟执行这个不纯的操作(惰性执行),包装当前的操
  • 把不纯的操作交给调用者来处理
  • 简单的讲 IO函子的map是纯函数 返回的都是IO函子对象 ,但是 IO 函子对象的_value 是不纯的 但是可以由使用者控制调用
const fp = require('lodash/fp')

class IO {
     
  // of方法快速创建IO,要一个值返回一个函数,将来需要值的时候再调用函数
  static of(value) {
     
    return new IO(() => value)
  }
  // 传入的是一个函数
  constructor (fn) {
     
    this._value = fn
  }

  map(fn) {
     
    // 这里用的是new一个新的构造函数,是为了把当前_value的函数和map传入的fn进行组合成新的函数
    return new IO(fp.flowRight(fn, this._value))
  }
}

// node执行环境可以传一个process对象(进程)
// 调用of的时候把当前取值的过程包装到函数里面,再在需要的时候再获取process
const r = IO.of(process)
  // map需要传入一个函数,函数需要接收一个参数,这个参数就是of中传递的参数process
  // 返回一下process中的execPath属性即当前node进程的执行路径
  .map(p => p.execPath)
console.log(r) // IO { _value: [Function] }


// 上面只是组合函数,如果需要调用就执行下面
console.log(r._value()) // C:\Program Files\nodejs\node.exe

Task函子(异步执行)

  • folktale 一个标准的函数式编程库。和 lodash、ramda 不同的是,他没有提供很多功能函数。只提供了一些函数式处理的操作,例如:compose、curry 等,一些函子 Task、Either、 MayBe 等
const {
      task } = require('folktale/concurrency/task')
const fs = require('fs')

function readFile (filename) {
     
  // task传递一个函数,参数是resolver
  // resolver里面有两个参数,一个是reject失败的时候执行的,一个是resolve成功的时候执行的
  return task(resolver => {
     
    //node中读取文件,第一个参数是路径,第二个是编码,第三个是回调,错误在先
    fs.readFile(filename, 'utf-8', (err, data) => {
     
      if(err) resolver.reject(err)
      resolver.resolve(data)
    })
  })
}

// readFile调用返回的是Task函子,调用要用run方法
readFile('package.json')
  //在run之前调用map方法,在map方法中会处理的拿到文件返回结果
  .map(split('\n'))
  .map(find(x => x.includes('version')))
  .run()
  // 现在没有对resolve进行处理,可以使用task的listen去监听获取的结果
  // listen传一个对象,onRejected是监听错误结果,onResolved是监听正确结果
  .listen({
     
    onRejected: (err) => {
     
      console.log(err)
    },
    onResolved: (value) => {
     
      console.log(value)
    }
  })

Pointed函子

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

Monad函子(单子)

函子是一个容器,可以包含任何值。函子之中再包含一个函子,也可以。但是,这样就会出现多层嵌套的函子。
Monad 函子的作用是,总是返回一个单层的函子
Monad 主要通过 join 和 flatMap两个方法实现解决函子嵌套的问题。

  • 当我们想要返回一个函数,这个函数返回一个值,这个时候可以调用map 方法
  • 当我们想要去合并一个函数,但是这个函数返回一个函子,这个时候我们要用flatMap 方法
const fp = require('lodash/fp')
const fs = require('fs')

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

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

  join () {
     
    return this._value()
  }

  // 同时调用map和join方法
  flatMap (fn) {
     
    return this.map(fn).join()
  }
}

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

let print = (x) => {
     
  return new IO(()=> {
     
    console.log(x)
    return x
  })
}

let r = readFile('package.json') // 得到一个_value值为 fs.readFileSync(filename, 'utf-8') 的IO函子
          // return this.map(fn--就是print函数) 
          //也就是把上面的文件读取函数执行结果作为x传入给print函数 
          //print函数.join() 其实就是执行 new IO(()=> {console.log(x); return x})
          .flatMap(print) 
          // 执行 ()=> {console.log(x); return x }
          .join()     
 
 
 r = readFile('package.json')
        // 处理数据,直接在读取文件之后,使用map进行处理即可
        .map(fp.toUpper)
        .flatMap(print)
        .join()  

你可能感兴趣的:(JS,学习回顾,js,nodejs,es6,javascript)